Getting Started
These are my notes from the ROS2 Navigation course available on The Construct. As this is a robotics note, you will need physical hardware components to follow along. Before each section though, I will list what is required before starting.
Requirements
Before starting, we need a robot with LIDAR and Odometry. We also need to use SLAM for localizing. Available ones are cartographer and SLAM-Toolbox. This note uses cartographer as the learning curve is lower. It is notable that cartographer is no longer maintained, so the better software to use is SLAM-Toolbox.
Cartographer is a system that provides real-time simultaneous localization and mapping (SLAM) in both 2D and 3D across multiple platforms and sensor configurations. Cartographer_ros is a ROS wrapper of cartographer so that we can integrate cartographer with ROS. This allows us to use prebuilt packages and only have to configure them.
Map
A map is a representation of the environment that the robot is functioning in. It uses the map to localize and plan trajectories. Maps are implemented in ROS2 via an occupancy grid where in each cell, specific values represent obstacles.
Configuring Cartographer
We use a launch file because we can start multiple nodes and can also set a node-specific parameter while launching a node. These parameters can then be loaded from a YAML file or specified in the launch file.
To launch cartographer
, we need to launch two nodes. Below are the files you need within their respective directories.
- cartographer.launch.py
- cartographer.lua
- config files
<package>/launch
import os
from ament_index_python.packages import get_package_share_directory
cartographer_config_dir = os.path.join(get_package_share_directory('cartographer_slam'), 'config')
Node(
package='cartographer_ros',
executable='cartographer_node',
name='cartographer_node',
output='screen',
parameters=[{'use_sim_time': True}], # boolean indicating node synchronization time with simulation time
arguments=['-configuration_directory', cartographer_config_dir,
'-configuration_basename', configuration_basename] # configuration_directory specifies where the configuration files are # configuration_basename specifies the configuration file name
),
Node(
package='cartographer_ros',
executable='cartographer_occupancy_grid_node',
output='screen',
name='occupancy_grid_node',
parameters=[{'use_sim_time': True}],
arguments=['-resolution', '0.05', '-publish_period_sec', '1.0'] # resolution specifies number of meters per grid in the map # publish_period_sec specifies frequency of publishing to /map topic
)
We then create a LUA file named cartographer.lua
within the <package>/config
directory.
include "map_builder.lua"
include "trajectory_builder.lua"
options = {
map_builder = MAP_BUILDER,
trajectory_builder = TRAJECTORY_BUILDER,
map_frame = "map",
tracking_frame = "base_footprint",
published_frame = "odom",
odom_frame = "odom",
provide_odom_frame = false,
publish_frame_projected_to_2d = true,
use_odometry = true,
use_nav_sat = false,
use_landmarks = false,
num_laser_scans = 1,
num_multi_echo_laser_scans = 0,
num_subdivisions_per_laser_scan = 1,
num_point_clouds = 0,
lookup_transform_timeout_sec = 0.2,
submap_publish_period_sec = 0.3,
pose_publish_period_sec = 5e-3,
trajectory_publish_period_sec = 30e-3,
rangefinder_sampling_ratio = 1.,
odometry_sampling_ratio = 1.,
fixed_frame_pose_sampling_ratio = 1.,
imu_sampling_ratio = 1.,
landmarks_sampling_ratio = 1.,
}
MAP_BUILDER.use_trajectory_builder_2d = true
TRAJECTORY_BUILDER_2D.min_range = 0.12
TRAJECTORY_BUILDER_2D.max_range = 3.5
TRAJECTORY_BUILDER_2D.missing_data_ray_length = 3.0
TRAJECTORY_BUILDER_2D.use_imu_data = false
TRAJECTORY_BUILDER_2D.use_online_correlative_scan_matching = true
TRAJECTORY_BUILDER_2D.motion_filter.max_angle_radians = math.rad(0.1)
POSE_GRAPH.constraint_builder.min_score = 0.65
POSE_GRAPH.constraint_builder.global_localization_min_score = 0.7
-- POSE_GRAPH.optimize_every_n_nodes = 0
return options
Make sure that within setup.py, we specify the new config and lua directories. If you're using C++, make sure to specify the DIRECTORIES within CMakeList.
setup.py
(os.path.join('share', package_name, 'launch'), glob('launch/*.launch.py')),
(os.path.join('share', package_name, 'config'), glob('config/*')),
CMakeList
install(DIRECTORY
launch
config
DESTINATION share/${PROJECT_NAME}/
)
Compilation
Compile the package and launch the launch file from the CLI. Once we've done that, run RVIZ2 on another terminal while our cartographer node is running.
To display our mapping, add the map
display and set the topic to /map
. If the mapping isn't showing, then the Quality of Service (QoS) parameters of the /map
may be wrong. We can add a couple more displays.
TF
: Allows us to see the frames of the robotLaserScan
: Allows us to see the laser coliding with the objects on the map
These are all specific to the hardware configurations and the topic may be dependent on your system.
Alternative Configurations
Cartographer works for many robots and sensors but must be configured properly to work optimally. By default, cartographer automatically subscribes to /scan, /odom, /imu
topics. All configurations can be given from the Lua configuration file. This means that in the launch files, we have to remap those three if we want to publish data to them.
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='cartographer_ros',
executable='cartographer_node',
name='cartographer',
remappings=[
('/scan', '/my_laser_scan'),
('/odom', '/my_odom'),
('/imu', '/my_imu')
]
)
])
Category | Parameter | Description | Default |
---|---|---|---|
Frames | map_frame | ROS frame ID for publishing sub-maps, the parent frame of poses. | map |
tracking_frame | Frame ID tracked by SLAM. IMU position if used, commonly | base_link | |
published_frame | Child frame for publishing poses. If | odom or base_link | |
odom_frame | Frame for publishing local SLAM result if | odom | |
provide_odom_frame | Publishes local non-loop-closed pose as | true | |
use_odometry | Subscribes to | true | |
use_nav_sat | Subscribes to | true | |
Laser Parameters | num_laser_scans | Number of laser scan topics to subscribe to. | 1 |
num_multi_echo_laser_scans | Number of multi-echo laser scan topics to subscribe to. | 0 | |
num_subdivisions_per_laser_scan | Number of point clouds to split each laser scan for unwarping during motion. | 1 | |
num_point_clouds | Number of point cloud topics to subscribe to. | 1 | |
Filter Parameters | lookup_transform_timeout_sec | Timeout in seconds for looking up transforms using tf2. | 0.2 |
submap_publish_period_sec | Interval in seconds to publish submap poses. | 0.3 | |
pose_publish_period_sec | Interval in seconds to publish poses (e.g., 5e-3 for 200 Hz). | 5e-3 | |
trajectory_publish_period_sec | Interval in seconds to publish trajectory markers (e.g., 30e-3 for 30 ms). | 30e-3 | |
odometry_sampling_ratio | Sampling ratio for odometry messages. | 1.0 | |
fixed_frame_sampling_ratio | Sampling ratio for fixed frame messages. | 1.0 | |
imu_sampling_ratio | Sampling ratio for IMU messages. | 1.0 | |
landmarks_sampling_ratio | Sampling ratio for landmark messages. | 1.0 | |
Trajectory Builder | TRAJECTORY_BUILDER_2D.min_range | Minimum distance measured to be considered for mapping. | 0.2 |
TRAJECTORY_BUILDER_2D.max_range | Maximum distance measured to be considered for mapping. | 30.0 | |
TRAJECTORY_BUILDER_2D.missing_data_ray_length | Distance to treat as a lost laser ray. | 25.0 | |
TRAJECTORY_BUILDER_2D.use_imu_data | Whether or not to use IMU data. | true |
If we want to save the map that we mapped out, we can run an executable named map_saver
from nav2_map_server
. It's important that we call this within the directory that we want to save the map in. The cartography has to be running for this to work.
ros2 run nav2_map_server map_saver_cli -f <name>
This'll generate two files of type .pgm
and .yaml
. The .pgm
is an image file of the map as an occupancy grid image. The .yaml
contains details about the resolution of the map.
image: turtlebot_area.pgm
mode: trinary
resolution: 0.05 # Resolution of the map (meters/pixel)
origin: [-3.8, -1.84, 0] # Coordinates of the lower-left pixel on the map (x,y,z). The third value indicates rotation and if there is none, value is 0
negate: 0 # Inverts the color of the map.
occupied_thresh: 0.65 # Pixels with a value greater than this value will be considered an occupied zone
free_thresh: 0.25 # Pixels with a value smaller than this will be considered a completely free zone
Using Saved Map
If we want to use the saved map, we have to launch map_server
and nav2_lifecycle_manager
. We can create a custom launch file for this, but we must specify some parameters.
from launch import LaunchDescription
from launch_ros.actions import Node
frmo ament_index_python.packages import get_package_share_directory
import os
def generate_launch_description():
map_file = os.path.join(get_package_share_directory('map_server'), 'config', 'turtlebot_area.yaml')
return LaunchDescription([
Node(
package="nav2_map_server",
executable="map_server",
name="map_server",
output="screen",
parameters=[{'use_sim_time': True}, # Boolean indicating if map_server must synchronize with its time with the simulation
{'yaml_filename':map_file] # The complete path to the map yaml file
),
Node(
package="nav2_lifecycle_manager",
executable="lifecycle_manager",
name="lifecycle_manager_mapper",
output="screen",
parameters=[{'use_sim_time': True},
{'autostart': True}, # lifecycle manager autostart when launched
{'node_names': ['map_server']}] # Nodes that the lifecycle manager takes care of
)
])
We can try using our saved map by creating a ROS2 package called map_server, placing our saved map files inside of config, and creating our launch file. Note that we can just put our launch file in any package and just create a new launch file with our configs.
You must specify your map display durability as Transient Local. Volatile means use incoming data being published as the map source, while transient local allows us to check if we had any cached messages that were published but not received by our subscriber as well as all incoming data.
Nav2 Lifecycle Manager
Navigation nodes are managed nodes meaning that they can be easily controlled to be restarted, paused, or running. They can do so by being at these states:
- Unconfigured
- Inactive
- Active
- Finalized
The diagram is taken from the course and depicts how nodes transition from one state to another. Seems to be like a state machine.
Managed nodes start at the unconfigured
state and is active usually when running. To transition to active
, nodes need an external agent that moves them to this state. Several nodes in Nav2, such as map_server
, amcl
, planner_server
, and controller_server
, are lifecycle enabled meaning they are managed nodes.
These nodes have the required overrides of the lifecycle functions:
- on_configure()
- on_activate()
- on_deactivate()
- on_cleanup()
- on_shutdown()
- on_error()
Lifecycle Manager
In Nav2, the external agent that helps transition nodes to an active state is called the nav2_lifecycle_manager
. The lifecycle manager changes the state of the managed nodes to achieve a controlled startup, shutdown, reset, pause, or resume of the navigation stack.
Referenced from Steve Macenski
Nav2 uses a wrapper of LifecycleNode
, the nav2_util LifecycleNode
and it hides many abstractions of LifecycleNodes for typical applications. It includes a bond connection for the lifecycle manager
to ensure that after a node transitions up, it also remains active. When a node crashes, it lets the lifecycle manager
know and transitions down the system to prevent a critical failure.
The nav2_lifecycle_manager
provides a ROS service from which other ROS nodes can invoke the startup, shutdown, reset, pause, or resume functions. Based on these requests, the nav2_lifecycle_manager
calls the necessary lifecycle services in the managed nodes. As shown previously in the nav2_lifecycle_manager
node within the launch file, we specify which nodes need management via node_names
. The ordering matters as that is the order in which they have to be moved to initialized (first to last) and the order in which they are paused and stopped (last to first).
It's important that the manager has the autostart
parameter which allows the nav2_lifecycle_manager
to do it's default behavior.
You interact with the nav2_lifecycle_manager
via a service named:
/<lifecycle_manager_node_name>/manage_nodes
ros2 service call /lifecycle_manager/manage_nodes nav2_msgs/srv/ManageLifecycleNodes "{command: 0}" # Start the nodes
ros2 service call /lifecycle_manager/manage_nodes nav2_msgs/srv/ManageLifecycleNodes "{command: 1}" # Pause the nodes
ros2 service call /lifecycle_manager/manage_nodes nav2_msgs/srv/ManageLifecycleNodes "{command: 2}" # Resume the nodes
ros2 service call /lifecycle_manager/manage_nodes nav2_msgs/srv/ManageLifecycleNodes "{command: 3}" # Reset the nodes
ros2 service call /lifecycle_manager/manage_nodes nav2_msgs/srv/ManageLifecycleNodes "{command: 4}" # Shutdown the nodes
This is because the service /manage_nodes
uses the message nav2_msgs/srv/ManageLifecycleNodes
which is as follows:
uint8 STARTUP = 0
uint8 PAUSE = 1
uint8 RESUME = 2
uint8 RESET = 3
uint8 SHUTDOWN = 4
uint8 command
---
bool success
Localization
Localization is knowing the current position with respect to the environment. This means knowing the position and orientation. This is otherwise known as the pose.
ROS has a very robust algorithm for localization. It uses AMCL
which is the Adaptive Monte-Carlo Licalization and uses a probabilistic localization system for a robot moving in 2D. It implements the adaptive or KLD sampling Monte Carlo localization approach which uses a particle filter to track a robot's pose against a known map.
A ROS Robot is localized when somebody publishes a transform between the /map
frame and the /odom
frame
That means that the /odom
frame of the robot knows its relative position to the /map
frame. Therefore, the robot knows its position on the map since /base_link
frame is directly connected to the /odom
frame.
When everything is correct, the AMCL publishes that transform.
ROS2 Localization
In order to localize, we need three nodes:
map_server
: provides the map to the localization algorithmlocalization
: the algorithmlife cycle manager
: state manager
Let's create an ament_cmake
package and the launch file for all three nodes.
- localization.launch.py
- amcl_config.yaml
- setup.py/CMakeLists
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch_ros.actions import Node
import os
def generate_launch_description():
nav2_yaml = os.path.join(get_package_share_directory('localization'), 'config', 'amcl_config.yaml')
map_file = os.path.join(get_package_share_directory('map_server'), 'config', 'turtlebot_area.yaml')
print(map_file)
return LaunchDescription([
Node(
package="nav2_map_server",
executable="map_server",
name="map_server",
output="screen",
parameters=[{'use_sim_time': True}, {'yaml_filename': map_file}] ## This is the saved map we got from our cartography.
),
Node(
package="nav2_amcl",
executable="amcl",
name="amcl",
output="screen",
parameters=[nav2_yaml]
),
Node(
package="nav2_lifecycle_manager",
Launch Fil executable="lifecycle_manager",
name="lifecycle_manager",
output="screen",
parameters=[{'use_sim_time': True},
{'autostart': True},
{'node_names': ['map_server', 'amcl']}]
)
])
The amcl_config specifies the configurations for the localization algorithm. This goes inside of the config directory of your package.
amcl:
ros__parameters:
use_sim_time: True
alpha1: 0.2
alpha2: 0.2
alpha3: 0.2
alpha4: 0.2
alpha5: 0.2
base_frame_id: "base_footprint"
beam_skip_distance: 0.5
beam_skip_error_threshold: 0.9
beam_skip_threshold: 0.3
do_beamskip: false
global_frame_id: "map"
lambda_short: 0.1
laser_likelihood_max_dist: 2.0
laser_max_range: 100.0
laser_min_range: -1.0
laser_model_type: "likelihood_field"
max_beams: 60
max_particles: 8000
min_particles: 200
odom_frame_id: "odom"
pf_err: 0.05
pf_z: 0.99
recovery_alpha_fast: 0.0
recovery_alpha_slow: 0.0
resample_interval: 1
robot_model_type: "nav2_amcl::DifferentialMotionModel"
save_pose_rate: 0.5
sigma_hit: 0.2
tf_broadcast: true
transform_tolerance: 1.0
update_min_a: 0.2
update_min_d: 0.25
z_hit: 0.5
z_max: 0.05
z_rand: 0.5
z_short: 0.05
If all is placed correctly, we should be able to receive our old map. From there onwards, you want to configure your RVIZ settings with these below:
map
: EnvironmentTF
: Frames of the robotRobot Model
: Robot structure (uses URDF model)Laser Scan
: Estimate of robot position relative to actual mapPose with Covariance
: Status of the localizationParticle Cloud
: See the localization particle distribution
Particle cloud uses the reliability policy of best effort and not volatile. If you want to use these configs again, you can save them.
We can use the 2D Pose Estimate
to tell our AMCL
system to start localizing the robot. You want to put it at a rough estimate of where the robot is so the algorithm can estimate roughly from the initial state. This'll start publishing the transform between map
and odom
topics respectively. When we say transform, it means relationship. The map and odom data are independent from oen another, but there is a mathematical relationship between them using AMCL, and can be used to "localize" our robot's position from these data. That's because we have laser scan (environment scanning) and odometry (wheel/position).
If we start moving our robot, our estimation and localization starts to fit better.
For 2D Localization, we only need and are the variables used for setting the robot in the map
frame. We can compute by using the AMCL.
The Adaptive Monte Carlo Localization algorithm uses particles to localize the robot. Each particle have their own coordinates, orientation value, and a weight. These weights are the likelihood of the particle being a good estimate of the robot's actual pose based on our sensor data. That means that if our weight is higher for a given particle, that particle is more likely to represent the robot's true pose. These particles are then re-sampled whenever the robot moves and with new sensor data, the higher likelihood particles remain and will converge over many iterations to the robot's true pose.
To get a good localization of our robot, we need to configure out AMCL node from our amcl_config.yaml. Below are the parameters and their respective categories.
Parameter | Type | Default | Description |
---|---|---|---|
min_particles | int | 500 | Minimum allowed number of particles. |
max_particles | int | 2000 | Maximum allowed number of particles. |
update_min_d | double | 0.25 meters | Translational movement required before performing a filter update. |
update_min_a | double | 0.2 radians | Rotational movement required before performing a filter update. |
resample_interval | int | 1 | Number of filter updates required before re-sampling. |
transform_tolerance | double | 1.0 seconds | Time to post-date the published transform, indicating validity into the future. |
recovery_alpha_slow | double | 0.0 (disabled) | Exponential decay rate for the slow average weight filter; helps decide recovery with random poses. Suggested: 0.001. |
recovery_alpha_fast | double | 0.0 (disabled) | Exponential decay rate for the fast average weight filter; helps decide recovery with random poses. Suggested: 0.1. |
set_initial_pose | bool | false | Causes AMCL to set the initial pose from |
initial_pose | Pose2D | 0 | X, Y, Z, and yaw coordinates of the robot base frame's initial pose in the global frame. |
always_reset_initial_pose | bool | false | Requires an initial pose on reset. Uses the last known pose if false. |
save_pose_rate | double | 0.5 Hz | Maximum rate at which to save the last estimated pose and covariance to the parameter server. Use -1.0 to disable. |
Parameter | Type | Default | Description |
---|---|---|---|
laser_min_range | double | -1.0 | Minimum scan range to be considered. Use laser's minimum range if set to -1.0. |
laser_max_range | double | 100 | Maximum scan range to be considered. Use laser's maximum range if set to -1.0. |
max_beams | int | 60 | Number of evenly-spaced beams in each scan used when updating the filter. |
z_hit | double | 0.5 | Mixture weight for z_hit part of the model. |
z_short | double | 0.05 | Mixture weight for the z_short part of the model. |
z_max | double | 0.05 | Mixture weight for the z_max part of the model. |
z_rand | double | 0.5 | Mixture weight for the z_rand part of the model. |
sigma_hit | double | 0.2 meters | Standard deviation for Gaussian model used in z_hit part. |
lambda_short | double | 0.1 | Exponential decay parameter for the z_short part of the model. |
laser_likelihood_max_dist | double | 2.0 meters | Maximum distance for obstacle inflation in the likelihood_field model. |
laser_model_type | string | "likelihood_field" | Model to use: |
Parameter | Type | Default | Description |
---|---|---|---|
robot_model_type | string | "differential" | Type of motion model: differential or omnidirectional . |
alpha1 | double | 0.2 | Noise in odometry's rotation estimate from rotational motion. |
alpha2 | double | 0.2 | Noise in odometry's rotation estimate from translational motion. |
alpha3 | double | 0.2 | Noise in odometry's translation estimate from translational motion. |
alpha4 | double | 0.2 | Noise in odometry's translation estimate from rotational motion. |
alpha5 | double | 0.2 | Translation-related noise parameter (only for omni model). |
odom_frame_id | string | "odom" | Frame to use for odometry. |
base_frame_id | string | "base_footprint" | Frame to use for the robot base. |
global_frame_id | string | "map" | Name of the coordinate frame published by the localization system. |
tf_broadcast | bool | true | Set to false to prevent AMCL from publishing the transform between global and odometry frames. |
Auto Localization
Via YAML
If you don't want to localize via GUI rviz
, you can do so by modifying the configuration settings for amcl
. When you start your localization server, you're given the initial_pose, to which you can set those within the amcl_config
:
set_initial_pose: true
initial_pose:
x: -4.44264
y: 2.32243
yaw: 0.328028
Rebuild the package, and you can start the server and not have to localize the robot.
Via CLI
ros2 topic pub -1 /initialpose geometry_msgs/msg/PoseWithCovarianceStamped "{header: {stamp: {sec: 0}, frame_id: 'map'}, pose: {pose: {position: {x: 0.2, y: 0.0, z: 0.0}, orientation: {w: 1.0}}}}"
Global Localization
Global localization is when neither the person or robot knows where it is on the map. In order to localize itself, it does global localization:
- Distribute all the particles of the filter across the map. (Calls the service
/reinitialize_global_localization
- Move the robot around until it detects where the it is located. If the environment is simple, can just rotate itself.
Modify the amcl_config to increase particles:
max_particles: 80000
min_particles: 2000
Call the service and start moving around to localize.
ros2 service call /reinitialize_global_localization std_srvs/srv/Empty
Path Planning
Path planning is planning a route from Point A to Point B, avoiding obstacles along the way.
In ROS2, to do path planning, we have to launch several nodes. Before we start any path planning, we need to have two nodes which provides the base map and localization:
Mapping Environment
nav2_map_server
: provides the base map as a topic and servicenav2_amcl
: responsible for localization via Adaptive Monte Carlo Localizer
To do path planning, we need these nodes:
Path Planning
planner
controller
manager_of_recovery_behaviors
behavior_tree_navigator
nav2_lifecycle_manager
- Planner
- Controller
- Navigation Controller
- Recoveries Server
The planner is responsible for computing a path from Point A to Point B. It can compute a path as soon as the robot receives a 2D_Goal_Pose
and also has access to a global environment representation and sensor data buffered into it ('Global Costmap`).
Currently on Galactic, there is only one Planenr algorithm available in ROS2, the Nav2Fn_Planner
.
In order to launch the planner, we need these in the launch file:
package='nav2_planner', these in the ```config``` directory.
<Tabs>
executable='planner_server',
name='planner_server',
output='screen',
parameters=[nav2_yaml] # contains all of the configuration parameters
nav2_yaml
planner_server:
ros__parameters:
expected_planner_frequency: 10.0
use_sim_time: True
planner_plugins: ["GridBased"]
GridBased:
plugin: "nav2_navfn_planner/NavfnPlanner"
tolerance: 0.5
use_astar: false
allow_unknown: true
The controller is responsible for performing reactive path planning from the current position to the maximum range of the sensors. From this data, it builds a trajectory to avoid the obstacles it encounters along the way while trying to follow the path generated by the planner. The controller is the one responsible for generating the wheel's commands.
There are only two planners available in ROS2:
dwb_controller
: used withdifferential drive
robotsTEB controller
: used withcar-like
robots (doesn't work properly in Galactic)
In order to launch the controller, we need these in the launch file:
package='nav2_controller',
executable='controller_server',
name='controller_server',
output='screen',
parameters=[controller_yaml] # contains all of the configuration parameters
controller_yaml
controller_server:
ros__parameters:
use_sim_time: True
controller_frequency: 10.0
min_x_velocity_threshold: 0.001
min_y_velocity_threshold: 0.5
min_theta_velocity_threshold: 0.001
failure_tolerance: 0.3
progress_checker_plugin: "progress_checker"
goal_checker_plugins: ["general_goal_checker"]
controller_plugins: ["FollowPath"]
# Progress checker parameters
progress_checker:
plugin: "nav2_controller::SimpleProgressChecker"
required_movement_radius: 0.5
movement_time_allowance: 10.0
# Goal checker parameters
general_goal_checker:
stateful: True
plugin: "nav2_controller::SimpleGoalChecker"
xy_goal_tolerance: 0.25
yaw_goal_tolerance: 0.25
# DWB parameters
FollowPath:
plugin: "dwb_core::DWBLocalPlanner"
debug_trajectory_details: True
min_vel_x: 0.0
min_vel_y: 0.0
max_vel_x: 0.26
max_vel_y: 0.0
max_vel_theta: 1.0
min_speed_xy: 0.0
max_speed_xy: 0.26
min_speed_theta: 0.0
acc_lim_x: 2.5
acc_lim_y: 0.0
- ```dwb_controller```: Used in ```differential drive``` robots acc_lim_theta: 3.2
decel_lim_x: -2.5
decel_lim_y: 0.0
decel_lim_theta: -3.2
vx_samples: 20
vy_samples: 5
vtheta_samples: 20
sim_time: 1.7
linear_granularity: 0.05
angular_granularity: 0.025
transform_tolerance: 0.2
xy_goal_tolerance: 0.25
trans_stopped_velocity: 0.25
short_circuit_trajectory_evaluation: True
stateful: True
critics: ["RotateToGoal", "Oscillation", "BaseObstacle", "GoalAlign", "PathAlign", "PathDist", "GoalDist"]
BaseObstacle.scale: 0.02
PathAlign.scale: 32.0
PathAlign.forward_point_distance: 0.1
GoalAlign.scale: 24.0
GoalAlign.forward_point_distance: 0.1
PathDist.scale: 32.0
GoalDist.scale: 24.0
RotateToGoal.scale: 32.0
RotateToGoal.slowing_factor: 5.0
RotateToGoal.lookahead_time: -1.0
The navigation controller coordinates between the planner node and the controller node. This node is called the bt_navigator
. It basically manages these two nodes.
Navigation controller launch file:
package='nav2_bt_navigator',
executable='bt_navigator',
name='bt_navigator',
output='screen',
parameters=[bt_navigator_yaml]
bt_navigator_yaml
bt_navigator:
ros__parameters:
use_sim_time: True
global_frame: map
robot_base_frame: base_link
odom_topic: /odom
bt_loop_duration: 10
default_server_timeout: 20
default_nav_to_pose_bt_xml: "/home/user/ros2_ws/src/path_planner_server/config/behavior.xml"
plugin_lib_names:
- nav2_compute_path_to_pose_action_bt_node
- nav2_compute_path_through_poses_action_bt_node
- nav2_smooth_path_action_bt_node
- nav2_follow_path_action_bt_node
- nav2_spin_action_bt_node
- nav2_wait_action_bt_node
- nav2_back_up_action_bt_node
- nav2_drive_on_heading_bt_node
- nav2_clear_costmap_service_bt_node
- nav2_is_stuck_condition_bt_node
- nav2_goal_reached_condition_bt_node
- nav2_goal_updated_condition_bt_node
- nav2_globally_updated_goal_condition_bt_node
- nav2_is_path_valid_condition_bt_node
- nav2_initial_pose_received_condition_bt_node
- nav2_reinitialize_global_localization_service_bt_node
- nav2_rate_controller_bt_node
- nav2_distance_controller_bt_node
- nav2_speed_controller_bt_node
- nav2_truncate_path_action_bt_node
- nav2_truncate_path_local_action_bt_node
- nav2_goal_updater_node_bt_node
- nav2_recovery_node_bt_node
- nav2_pipeline_sequence_bt_node
- nav2_round_robin_node_bt_node
- nav2_transform_available_condition_bt_node
- nav2_time_expired_condition_bt_node
- nav2_path_expiring_timer_condition
- nav2_distance_traveled_condition_bt_node
- nav2_single_trigger_bt_node
- nav2_goal_updated_controller_bt_node
- nav2_is_battery_low_condition_bt_node
- nav2_navigate_through_poses_action_bt_node
- nav2_navigate_to_pose_action_bt_node
- nav2_remove_passed_goals_action_bt_node
- nav2_planner_selector_bt_node
- nav2_controller_selector_bt_node
- nav2_goal_checker_selector_bt_node
- nav2_controller_cancel_bt_node
- nav2_path_longer_on_approach_bt_node
- nav2_wait_cancel_bt_node
- nav2_spin_cancel_bt_node
- nav2_back_up_cancel_bt_node
- nav2_drive_on_heading_cancel_bt_node
The behavior of this node is defined by the XML file dubbed behavior.xml
which contains the behavior tree. This behavior tree includes when to call the path planner, when to call the controller, and when to activate a recovery behavior. More on behavior trees later on in this note.
behavior.xml
<!--
This Behavior Tree replans the global path periodically at 1 Hz, and has
recovery actions. Obtained from the official Nav2 package
-->
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<RecoveryNode number_of_retries="6" name="NavigateRecovery">
<PipelineSequence name="NavigateWithReplanning">
<RateController hz="1.0">
<RecoveryNode number_of_retries="1" name="ComputePathToPose">
<ComputePathToPose goal="{goal}" path="{path}" planner_id="GridBased"/>
<ClearEntireCostmap service_name="global_costmap/clear_entirely_global_costmap"/>
</RecoveryNode>
</RateController>
<RecoveryNode number_of_retries="1" name="FollowPath">
<FollowPath path="{path}" controller_id="FollowPath"/>
<ClearEntireCostmap service_name="local_costmap/clear_entirely_local_costmap"/>
</RecoveryNode>
</PipelineSequence>
<SequenceStar name="RecoveryActions">
<ClearEntireCostmap service_name="local_costmap/clear_entirely_local_costmap"/>
<ClearEntireCostmap service_name="global_costmap/clear_entirely_global_costmap"/>
<Spin spin_dist="1.57"/>
<Wait wait_duration="5"/>
</SequenceStar>
</RecoveryNode>
</BehaviorTree>
</root>
When a robot cannot find a valid path to the goal provided or gets stuck in the middle, we use recovery behaviors provided by recovery servers. These are simple predefined movements called via behavior trees. When the robot gets stuck, the bt_navigator
will call the recoverieS_server
.
To Launch Recovery Servers:
package='nav2_behaviors',
executable='behavior_server', these in the ```config``` directory.
<Tabs>
name='behavior_server',
output='screen',
parameters=[recovery_yaml]
There are three recovery behaviors available in the Nav2 stack:
- spin: performs an in-place rotation by a given angle
- backup: performs a linear translation by a given distance
- wait: brings the robot to a stationary state
We can indicate which one of these behaviors we want to load within the config file recovery_yaml
.
recovery_yaml
behavior_server:
ros__parameters:
costmap_topic: local_costmap/costmap_raw
footprint_topic: local_costmap/published_footprint
cycle_frequency: 10.0
behavior_plugins: ["spin", "backup", "wait"]
spin:
plugin: "nav2_behaviors/Spin"
backup:
plugin: "nav2_behaviors/BackUp"
wait:
plugin: "nav2_behaviors/Wait"
global_frame: odom
robot_base_frame: base_link
transform_timeout: 0.1
use_sim_time: true
simulate_ahead_time: 2.0
max_rotational_vel: 1.0
min_rotational_vel: 0.4
rotational_acc_lim: 3.2
Demo
We'll start by first creating a new package using our favorite CMake build system. We'll then create two directories: config
and launch
.
We then create these and place them in their respective directories.
- pathplanner.launch.py
- planner.yaml
- controller.yaml
- bt_navigator.yaml
- recovery.yaml
- behavior.xml
from launch import LaunchDescription
from launch_ros.actions import Node
from ament_index_python.packages import get_package_share_directory
import os
def generate_launch_description():
planner_yaml = os.path.join(get_package_share_directory('path_planner_server'), 'config', 'planner.yaml')
controller_yaml = os.path.join(get_package_share_directory('path_planner_server'), 'config', 'controller.yaml')
recovery_yaml = os.path.join(get_package_share_directory('path_planner_server'), 'config', 'recovery.yaml')
bt_navigator_yaml = os.path.join(get_package_share_directory('path_planner_server'), 'config', 'bt_navigator.yaml')
return LaunchDescription([
Node(
package='nav2_planner',
executable='planner_server',
name='planner_server',
output='screen',
parameters=[planner_yaml]
),
Node(
package='nav2_controller',
executable='controller_server',
name='controller_server',
output='screen', these in the ```config``` directory.
<Tabs> these in the ```config``` directory.
<Tabs>
parameters=[controller_yaml],
),
Node(
package='nav2_bt_navigator',
executable='bt_navigator',
name='bt_navigator',
output='screen',
parameters=[bt_navigator_yaml],
),
Node(
package='nav2_behaviors',
executable='behavior_server',
name='behavior_server',
output='screen',
parameters=[recovery_yaml],
),
Node(
package='nav2_lifecycle_manager',
executable='lifecycle_manager',
name='lifecycle_manager_pathplanner',
output='screen',
parameters=[{'autostart': True},
{'node_names': ['planner_server', 'controller_server', 'behavior_server', 'bt_navigator']}],
)
])
planner_server:
ros__parameters:
expected_planner_frequency: 10.0
use_sim_time: True
planner_plugins: ["GridBased"]
GridBased:
plugin: "nav2_navfn_planner/NavfnPlanner"
tolerance: 0.5
use_astar: false
allow_unknown: true
controller_server:
ros__parameters:
use_sim_time: True
controller_frequency: 10.0
min_x_velocity_threshold: 0.001
min_y_velocity_threshold: 0.5
min_theta_velocity_threshold: 0.001
failure_tolerance: 0.3
progress_checker_plugin: "progress_checker"
goal_checker_plugins: ["general_goal_checker"]
controller_plugins: ["FollowPath"]
progress_checker:
plugin: "nav2_controller::SimpleProgressChecker"
required_movement_radius: 0.5
movement_time_allowance: 10.0
general_goal_checker:
stateful: True
plugin: "nav2_controller::SimpleGoalChecker"
xy_goal_tolerance: 0.25
yaw_goal_tolerance: 0.25
FollowPath:
plugin: "dwb_core::DWBLocalPlanner"
debug_trajectory_details: True
min_vel_x: 0.0
min_vel_y: 0.0
max_vel_x: 0.26
max_vel_y: 0.0
max_vel_theta: 1.0
min_speed_xy: 0.0
max_speed_xy: 0.26
min_speed_theta: 0.0
acc_lim_x: 2.5
acc_lim_y: 0.0
acc_lim_theta: 3.2
decel_lim_x: -2.5
decel_lim_y: 0.0
decel_lim_theta: -3.2
vx_samples: 20
vy_samples: 5
vtheta_samples: 20
sim_time: 1.7
linear_granularity: 0.05
angular_granularity: 0.025
transform_tolerance: 0.2
xy_goal_tolerance: 0.25
trans_stopped_velocity: 0.25
short_circuit_trajectory_evaluation: True
stateful: True
critics: ["RotateToGoal", "Oscillation", "BaseObstacle", "GoalAlign", "PathAlign", "PathDist", "GoalDist"]
BaseObstacle.scale: 0.02
PathAlign.scale: 32.0
PathAlign.forward_point_distance: 0.1
GoalAlign.scale: 24.0
GoalAlign.forward_point_distance: 0.1
PathDist.scale: 32.0
GoalDist.scale: 24.0
RotateToGoal.scale: 32.0
RotateToGoal.slowing_factor: 5.0
RotateToGoal.lookahead_time: -1.0
bt_navigator:
ros__parameters:
use_sim_time: True
global_frame: map
robot_base_frame: base_link
odom_topic: /odom
bt_loop_duration: 10
default_server_timeout: 20
default_nav_to_pose_bt_xml: "/home/user/ros2_ws/src/path_planner_server/config/behavior.xml"
plugin_lib_names:
- nav2_compute_path_to_pose_action_bt_node
- nav2_compute_path_through_poses_action_bt_node
- nav2_smooth_path_action_bt_node
- nav2_follow_path_action_bt_node
- nav2_spin_action_bt_node
- nav2_wait_action_bt_node
- nav2_back_up_action_bt_node
- nav2_drive_on_heading_bt_node
- nav2_clear_costmap_service_bt_node
- nav2_is_stuck_condition_bt_node
- nav2_goal_reached_condition_bt_node
- nav2_goal_updated_condition_bt_node
- nav2_globally_updated_goal_condition_bt_node
- nav2_is_path_valid_condition_bt_node
- nav2_initial_pose_received_condition_bt_node
- nav2_reinitialize_global_localization_service_bt_node
- nav2_rate_controller_bt_node
- nav2_distance_controller_bt_node
- nav2_speed_controller_bt_node
- nav2_truncate_path_action_bt_node
- nav2_truncate_path_local_action_bt_node
- nav2_goal_updater_node_bt_node
- nav2_recovery_node_bt_node
- nav2_pipeline_sequence_bt_node
- nav2_round_robin_node_bt_node
- nav2_transform_available_condition_bt_node
- nav2_time_expired_condition_bt_node
- nav2_path_expiring_timer_condition
- nav2_distance_traveled_condition_bt_node
- nav2_single_trigger_bt_node
- nav2_goal_updated_controller_bt_node
- nav2_is_battery_low_condition_bt_node
- nav2_navigate_through_poses_action_bt_node
- nav2_navigate_to_pose_action_bt_node
- nav2_remove_passed_goals_action_bt_node
- nav2_planner_selector_bt_node
- nav2_controller_selector_bt_node
- nav2_goal_checker_selector_bt_node
- nav2_controller_cancel_bt_node
- nav2_path_longer_on_approach_bt_node
- nav2_wait_cancel_bt_node
- nav2_spin_cancel_bt_node
- nav2_back_up_cancel_bt_node
- nav2_drive_on_heading_cancel_bt_node
behavior_server:
ros__parameters:
costmap_topic: local_costmap/costmap_raw
footprint_topic: local_costmap/published_footprint
cycle_frequency: 10.0
behavior_plugins: ["spin", "backup", "wait"]
spin:
plugin: "nav2_behaviors/Spin"
backup:
plugin: "nav2_behaviors/BackUp"
wait:
plugin: "nav2_behaviors/Wait"
global_frame: odom
robot_base_frame: base_link
transform_timeout: 0.1
use_sim_time: true
simulate_ahead_time: 2.0
max_rotational_vel: 1.0
min_rotational_vel: 0.4
rotational_acc_lim: 3.2
<!--
This Behavior Tree replans the global path periodically at 1 Hz, and has
recovery actions. Obtained from the official Nav2 package
-->
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<RecoveryNode number_of_retries="6" name="NavigateRecovery">
<PipelineSequence name="NavigateWithReplanning">
<RateController hz="1.0">
<RecoveryNode number_of_retries="1" name="ComputePathToPose">
<ComputePathToPose goal="{goal}" path="{path}" planner_id="GridBased"/>
<ClearEntireCostmap service_name="global_costmap/clear_entirely_global_costmap"/>
</RecoveryNode>
</RateController>
<RecoveryNode number_of_retries="1" name="FollowPath">
<FollowPath path="{path}" controller_id="FollowPath"/>
<ClearEntireCostmap service_name="local_costmap/clear_entirely_local_costmap"/>
</RecoveryNode>
</PipelineSequence>
<SequenceStar name="RecoveryActions">
<ClearEntireCostmap service_name="local_costmap/clear_entirely_local_costmap"/>
<ClearEntireCostmap service_name="global_costmap/clear_entirely_global_costmap"/>
<Spin spin_dist="1.57"/>
<Wait wait_duration="5"/>
</SequenceStar>
</RecoveryNode>
</BehaviorTree>
</root>
We have to be running in parallel our localization server, the one responsible for localizing our robot with the map and the one providing the base map environment.
RVIZ2 Settings
On top of our settings provided for localization, we add the Path
display and change LineStyle to BillBoards
. If everything is set, we can get something cool like below!
Navigation Goal via CLI
We can send a goal via the CLI through the server interface /navigate_to_pose
or goal_pose
.
- Action
- Topic
- ROS2 Node
ros2 action send_goal /navigate_to_pose nav2_msgs/action/NavigateToPose "pose: {header: {frame_id: map}, pose: {position: {x: 1.52, y: 1.92, z: 0.0}, orientation:{x: 0.0, y: 0.0, z: 0, w: 1.0000000}}}"
ros2 topic pub -1 /goal_pose geometry_msgs/PoseStamped "{header: {stamp: {sec: 0}, frame_id: 'map'}, pose: {position: {x: 2.2, y: 0.0, z: 0.0}, orientation: {w: 1.0}}}"
Taken directly from The Construct.
import rclpy
from nav2_msgs.action import NavigateToPose
from rclpy.action import ActionClient
from rclpy.node import Node
from geometry_msgs.msg import PointStamped
class NavToPoseActionClient(Node):
def __init__(self):
super().__init__('Nav_To_Pose_Action_Client')
self._action_client = ActionClient(self, NavigateToPose, '/navigate_to_pose')
self.subscriber_ = self.create_subscription(PointStamped, 'clicked_point', self.callback, 1)
self.subscriber_ # prevent unused variable warning
def callback(self, msg):
self.get_logger().info('Recieved Data:\n X : %f \n Y : %f \n Z : %f' % (msg.point.x, msg.point.y, msg.point.z))
self.send_goal (msg.point.x, msg.point.y, 0.0)
def send_goal(self, x ,y, theta):
self.get_logger().info('sending goal to action server')
goal_pose = NavigateToPose.Goal()
goal_pose.pose.header.frame_id = 'map'
goal_pose.pose.pose.position.x = x
goal_pose.pose.pose.position.y = y
goal_pose.pose.pose.position.z = theta
self.get_logger().info('waiting for action server')
self._action_client.wait_for_server()
self.get_logger().info('action server detected')
self._send_goal_future = self._action_client.send_goal_async(
goal_pose,
feedback_callback=self.feedback_callback)
self.get_logger().info('goal sent')
self._send_goal_future.add_done_callback(self.goal_response_callback)
def goal_response_callback(self, future):
goal_handle = future.result()
if not goal_handle.accepted:
self.get_logger().info('Goal rejected :(')
return
self.get_logger().info('Goal accepted :)')
self._get_result_future = goal_handle.get_result_async()
self._get_result_future.add_done_callback(self.get_result_callback)
def get_result_callback(self, future):
result = future.result().result
self.get_logger().info('Result: {0}' + str(result))
rclpy.shutdown()
def feedback_callback(self, feedback_msg):
feedback = feedback_msg.feedback
self.get_logger().info('FEEDBACK:' + str(feedback) )
def main(args=None):
rclpy.init(args=args)
action_client = NavToPoseActionClient()
rclpy.spin(action_client)
if __name__ == '__main__':
main()
Used via RVIZ and Publish Point
feature.
Obstacle Avoidance
The Nav2 stack avoids dynamic and static obstacles by utilizing what's known as costmaps.
Costmaps
A Costmap is a 2D representation of robot-sensed obstacles on a grid map. Each grid cells contain information about the obstacles detected by the sensors. The cost of the cell can be unknown, free, occupied, or inflated. Different colors indicate how likely it is to colide against an obstacle. Using this information, the Nav2 stack can compute their tasks safely and efficiently.
There are two kinds of costmaps: global
and local
.
global
: generated from obstacles on a static map and is used by theplanner
to generate long term path.local
: created from new obstacles during runtime(sensors) and is used by thecontroller
to generate short-term path
Global Costmaps
To add a global costmap
, we can add these parameters to the planner.yaml
file.
global_costmap:
global_costmap:
ros__parameters:
update_frequency: 1.0
publish_frequency: 1.0
global_frame: map
robot_base_frame: base_link
use_sim_time: True
robot_radius: 0.15
resolution: 0.05
track_unknown_space: true
plugins: ["static_layer", "inflation_layer"]
static_layer:
plugin: "nav2_costmap_2d::StaticLayer"
map_subscribe_transient_local: True
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
cost_scaling_factor: 3.0
inflation_radius: 0.35
always_send_full_costmap: True
When we run our path_planner_server
with these new parameters, we see an added map on top of our base map. When we add a new obstacle, the global costmap is not updated. The differences can be seen below. This means that our planner will not take into consideration the new dynamic obstacle that spawned in and will crash.
The different tones of gray are to illustrate the different regions the robot can use. Dark tones of grey means the robot cannot use that area to path plan as it is too close to an obstacle
Global Costmap Obstacle Layers
Global costmaps are generated as the superposition of different obstacle layers. This means that each layer adds a set of obstacles to the global costmap
based on how the layer computes the obstacles.
Available Costmap layers:
Static Layer
: Adds as obstacles to theglobal costmap
any black dot that exists in the static mapInflation Layer
: Adds an inflation to any obstacle in theglobal costmap
, as a safe distance to maintainObstacle Layer
: Adds to theglobal costmap
any object detected by a 2D sensorVoxel Layer
: Adds to theglobal costmap
3D obstacles from PointCloud data.
We can specify which layers or all in our parameter list via plugins.
plugins: ["static_layer", "obstacle_layer", "inflation_layer", "voxel_layer"] # Order matters here, inflation is applied after obstacle, etc.
Adding these layers will allow us to account for dynamic obstacles.
General Parameters
- global_costmap
- static_layer
- inflation_layer
- obstacle_layer
global_costmap:
global_costmap:
ros__parameters:
update_frequency: 1.0
publish_frequency: 1.0
global_frame: map
robot_base_frame: base_link
use_sim_time: True
robot_radius: 0.15
resolution: 0.05
track_unknown_space: true
plugins: ["static_layer", "obstacle_layer", "inflation_layer"]
Represents a largely unchanging portion of the Costmap, like those generated
by SLAM. yaml static_layer: plugin: "nav2_costmap_2d::StaticLayer" map_subscribe_transient_local: True
Adds new values around lethal obstacles to make the Costmap represent robot's
configuration space. yaml inflation_layer: plugin: "nav2_costmap_2d::InflationLayer" cost_scaling_factor: 3.0 inflation_radius: 0.35
Tracks obstacles read by sensor data and marks/raytraces obstacles in 2D.
observation_sources: scan scan: topic: /scan max_obstacle_height: 2.0
clearing: True marking: True data_type: "LaserScan" raytrace_max_range: 3.0
raytrace_min_range: 0.0 obstacle_max_range: 2.5 obstacle_min_range: 0.0 ```
</TabItem>
<TabItem value="voxel_layer" label="voxel_layer">
Responsible for including 3D detected obstacles in the 2D costmaps.
```yaml
voxel_layer:
plugin: "nav2_costmap_2d::VoxelLayer"
enabled: True
footprint_clearing_enabled: true
max_obstacle_height: 2.0
publish_voxel_map: True
origin_z: 0.0
z_resolution: 0.05
z_voxels: 16
max_obstacle_height: 2.0
unknown_threshold: 15
mark_threshold: 0
observation_sources: pointcloud
combination_method: 1
pointcloud: # no frame set, uses frame from message
topic: /intel_realsense_r200_depth/points
max_obstacle_height: 2.0
min_obstacle_height: 0.0
obstacle_max_range: 2.5
obstacle_min_range: 0.0
raytrace_max_range: 3.0
raytrace_min_range: 0.0
clearing: True
marking: True
data_type: "PointCloud2"
Local Costmap
Let's figure out how to add a local costmap
. It's no different than a global costmap
but goes inside of controller.yaml
instead of planner.yaml
. This is because a local costmap
is a dynamic version of the global costmap
using sensors.
local_costmap:
local_costmap:
ros__parameters:
update_frequency: 5.0
publish_frequency: 2.0
global_frame: odom
robot_base_frame: base_link
use_sim_time: True
rolling_window: true
width: 1
height: 1
resolution: 0.05
robot_radius: 0.15
plugins: ["voxel_layer", "inflation_layer"]
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
cost_scaling_factor: 3.0
inflation_radius: 0.35
voxel_layer:
plugin: "nav2_costmap_2d::VoxelLayer"
enabled: True
publish_voxel_map: True
origin_z: 0.0
z_resolution: 0.05
z_voxels: 16
max_obstacle_height: 2.0
mark_threshold: 0
observation_sources: scan
scan:
topic: /scan
max_obstacle_height: 2.0
clearing: True
marking: True
data_type: "LaserScan"
raytrace_max_range: 3.0
raytrace_min_range: 0.0
obstacle_max_range: 2.5
obstacle_min_range: 0.0
static_layer:
map_subscribe_transient_local: True
always_send_full_costmap: True
The robot's shape can influence what extent the obstacles in the Costmaps will be inflated to. This is then taken into consideration when computing a path to said point. We can specify our robot's shape within our costmap parameters in both local and global.
When we use round robots, we use the parameter robot_radius
. For more complex robots that aren't round, we have to use footprint
parameter instead that specifies the coordinates for each point of the robot. The footprint
parameter is the project of the robot on the ground. For example, if a square robot has 65 cm per side, we would specify:
footprint: "[[0.325, 0.325], [0.325, -0.325], [-0.325, -0.325], [-0.325, 0.325]]"
We also have to change the controller.yaml
configuration:
BaseObstacle
if usingrobot_radius
ObstacleFootprint
if usingfootprint
Remember that we can only specify one parameter: footprint
or robot_radius
.
Demo
Path Planner Package
Currently, we have two packages: localization
and path_planner
. One does the localization and mapping while the other does the path planning. Ideally, for a full navigaiton system, we want a single package with one nav2_lifecycle_manager
managing all navigation nodes.
How you wanna structure it is up to you, and having separate packages can be good for debugging, but ideally, in real navigation systems for deployment, you want one package for ease of use with one single launch file.
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
controller_yaml = os.path.join(get_package_share_directory('path_planner_server'), 'config', 'controller.yaml')
bt_navigator_yaml = os.path.join(get_package_share_directory('path_planner_server'), 'config', 'bt_navigator.yaml')
planner_yaml = os.path.join(get_package_share_directory('path_planner_server'), 'config', 'planner_server.yaml')
recovery_yaml = os.path.join(get_package_share_directory('path_planner_server'), 'config', 'recovery.yaml')
amcl_yaml = os.path.join(get_package_share_directory('localization_server'), 'config', 'amcl_config.yaml')
map_file = os.path.join(get_package_share_directory('map_server'), 'config', 'turtlebot_area.yaml')
return LaunchDescription([
Node(
package='nav2_map_server',
executable='map_server',
name='map_server',
output='screen',
parameters=[{'use_sim_time': True},
{'yaml_filename':map_file}]
),
Node(
package='nav2_amcl',
executable='amcl',
name='amcl',
output='screen',
parameters=[amcl_yaml]
),
Node(
package='nav2_controller',
executable='controller_server',
name='controller_server',
output='screen',
parameters=[controller_yaml]),
Node(
package='nav2_planner',
executable='planner_server',
name='planner_server',
output='screen',
parameters=[planner_yaml]),
Node(
package='nav2_behaviors',
executable='behavior_server',
name='recoveries_server',
parameters=[recovery_yaml],
output='screen'),
Node(
package='nav2_bt_navigator',
executable='bt_navigator',
name='bt_navigator',
output='screen',
parameters=[bt_navigator_yaml]),
Node(
package='nav2_lifecycle_manager',
executable='lifecycle_manager',
name='lifecycle_manager',
output='screen',
parameters=[{'autostart': True},
{'node_names': ['map_server',
'amcl',
'controller_server',
'planner_server',
'recoveries_server',
'bt_navigator']}])
])
Multi-robot Navigation
In the scenario you have multiple robots, each robot will have the same /cmd_vel
topic name, same base_footprint
frame name and even the same control node name. This is complicated because the navigation stack won't know which robot it's talking to. We can bypass this by using namespaces
which are labels we assign to each robot. This means that each robot may have the same name but will have a unique namespace
for it. namespaces
are configured at:
- Simulated Robots: At the URDF description file and the Gazebo plugins
- Real Robots: At the bring-up routine provided by the vendor
If these are not configured properly, the robots will have difficulty booting into namespaces
mode. For two robots, we could have the following:
/clock
/parameter_events
/performance_metrics
/rosout
/tb3_0/cmd_vel
/tb3_0/imu
/tb3_0/joint_states
/tb3_0/odom
/tb3_0/robot_description
/tb3_0/scan
/tb3_1/cmd_vel
/tb3_1/imu
/tb3_1/joint_states
/tb3_1/odom
/tb3_1/robot_description
/tb3_1/scan
/tf
/tf_static
Configuration
To have a multiagent system that uses the same nav stack, we need for each robot:
localization
controller_server
planner_server
bt_navigator
The only thing that'll be used by all is the map_server
as all robots will use the same map.
Mapping
It is possible to have multiple robots collaborate to create a single map between them but that is covered later on. This tutorial goes over:
- One robot from the fleet in charge of making the robot
- A single
map_server
is launched using that map - All the navigation systems for each robot will use the same name by asking for the same
map_server
Multirobot cartographer
There isn't much difference when trying to do multirobot navigation. For mapping, al lwe do is add and modify a few lines of code. For this, we use our cartographer config files and simply add these few lines of code below.
- cartographer_multi.lua
- multi_cartographer.launch.py
- Step 2
- Step 3
- Step 4
tracking_frame = "tb3_0/base_footprint",
published_frame = "tb3_0/odom",
odom_frame = "tb3_0/odom"
('/odom', '/tb3_0/odom'), ('/scan', '/tb3_0/scan'), ] ```
</TabItem>
</Tabs>
:::warn
When localizing with one single robot as above and having the other stand still, the map will think the idle robot is an obstacle.
:::
### **Localization**
We can launch a localization system for each robot. This means launching two `amcl` nodes each configured for their respective robot.
<Tabs>
<TabItem value="Step 1" label="Step 1">
- Add a ```namespace``` variable to the launches of ```amcl``` nodes.
- Create a new launch file named ```multi_localization.launch.py``` to include two ```amcl``` nodes, each with a different namespace, according to the robot it will localize. Note that each node's parameters argument must point to **two** config files.
- Try to use the cleaned map that doesn't include the idle robot as an obstacle. You can edit the ```.pgm``` file using ```GIMP```.
```py
Node(
namespace="tb3_0",
package="nav2_amcl",
executable="amcl",
name="amcl",
output="screen",
parameters=[tb3_0_config],
),
Node(
namespace="tb3_1",
package="nav2_amcl",
executable="amcl",
name+"amcl",
output="screen",
parameters=[tb3_1_config],
),
- Create two copies of the
amcl_config.yaml
file, with each one of the names referencing the robot it controls. Example:tb3_0_amcl_config.yaml
- Modify the
amcl_config.yaml
files to make sure it fits the robot needs. -
- Modifying the first line to contain the full new name of the node.
-
- All frames must be modified to include the
namespace
. The only frame that doesn't have to be changed is theglobal_frame_id
sincei t's a global single frame for all the robots.
- All frames must be modified to include the
-
- Topics don't need to be modified because the
namespace
arguments of the launch file automatically modifies them. This doesn't apply to frames.
- Topics don't need to be modified because the
-
- The
map_topic
parameters must be forced to/map
. If we don't do this with/
, then it automatically usestb3_0/map
as the topic which isn't the topic the map server publishes.
- The
- Import the two config files into the
multi_localization.launch.py
- Assign it to their respective nodes.
# tb3_0_amcl_config.yaml
tb3_0/amcl:
ros__parameters:
use_sim_time: True
alpha1: 0.2
alpha2: 0.2
alpha3: 0.2
alpha4: 0.2
alpha5: 0.2
base_frame_id: "tb3_0/base_footprint"
beam_skip_distance: 0.5
beam_skip_error_threshold: 0.9
beam_skip_threshold: 0.3
do_beamskip: false
global_frame_id: "map"
lambda_short: 0.1
laser_likelihood_max_dist: 2.0
laser_max_range: 100.0
laser_min_range: -1.0
laser_model_type: "likelihood_field"
max_beams: 60
max_particles: 8000
min_particles: 200
odom_frame_id: "tb3_0/odom"
pf_err: 0.05
pf_z: 0.99
recovery_alpha_fast: 0.0
recovery_alpha_slow: 0.0
resample_interval: 1
robot_model_type: "nav2_amcl::DifferentialMotionModel"
save_pose_rate: 0.5
sigma_hit: 0.2
tf_broadcast: true
transform_tolerance: 1.0
update_min_a: 0.2
update_min_d: 0.25
z_hit: 0.5
z_max: 0.05
z_rand: 0.5
z_short: 0.05
scan_topic: "/tb3_0/scan"
map_topic: "/map"
set_initial_pose: true
initial_pose:
x: 0.0
y: 0.0
yaw: 0.0
- Add the full node names to the lifecycle manager with their
namespaces
We have to add a new parameter {'bond_timeout': 0.0}
to prevent launching errors
Node(
package='nav2_lifecycle_manager',
executable='lifecycle_manager',
name='lifecycle_manager_localization',
output='screen',
parameters=[{'use_sim_time': True},
{'autostart': True},
{'bond_timeout':0.0},
{'node_names': ['map_server', 'tb3_0/amcl', 'tb3_1/amcl']}]
)
- Modify the
map_server
launch file. - Specify the
topic_name
andframe_id
of themap_server
. These parameters aren't configured when we do a single robotmap_server
.
Node(
package='nav2_map_server',
executable='map_server',
name='map_server',
output='screen',
parameters=[{'use_sim_time': True},
{'topic_name':"map"},
{'frame_id':"map"},
{'yaml_filename':map_file}]
),
The final launch file should be:
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
tb3_0_nav2_yaml = os.path.join(get_package_share_directory('localization_server'), 'config', 'tb3_0_amcl_config.yaml')
tb3_1_nav2_yaml = os.path.join(get_package_share_directory('localization_server'), 'config', 'tb3_1_amcl_config.yaml')
map_file = os.path.join(get_package_share_directory('map_server'), 'config', 'turtlebot_area_two_robots_clean.yaml')
return LaunchDescription([
Node(
package='nav2_map_server',
executable='map_server',
name='map_server',
output='screen',
parameters=[{'use_sim_time': True},
{'topic_name':"map"},
{'frame_id':"map"},
{'yaml_filename':map_file}]
),
Node(
namespace='tb3_0',
package='nav2_amcl',
executable='amcl',
name='amcl',
output='screen',
parameters=[tb3_0_nav2_yaml]
),
Node(
namespace='tb3_1',
package='nav2_amcl',
executable='amcl',
name='amcl',
output='screen',
parameters=[tb3_1_nav2_yaml]
),
Node(
package='nav2_lifecycle_manager',
executable='lifecycle_manager',
name='lifecycle_manager_localization',
output='screen',
parameters=[{'use_sim_time': True},
{'autostart': True},
{'bond_timeout':0.0},
{'node_names': ['map_server', 'tb3_0/amcl', 'tb3_1/amcl']}]
)
])
Configuring RVIZ2
- Load the RVIZ configuration of the localization unit:
.rviz
- Modify the topic of the
LaserScan
to point to thetb3_0/scan
and indicate theColor Transformer
asFlatColor
. - Add another
LaserScan
and make ittb3_1/scan
withColor Transformer
as the same as previous but different color. - Modify the
RobotModel
display as below: -
- In the
Description Source
, selectTopic
and indicate/tb3_0/robot_description
.
- In the
-
- In the
TF prefix
, indicate the namespace of the robot, which for tb3_0 istb3_0
.
- In the
- Do the same for
tb3_1
. - Add the
Panel
ofTool Properties
and modify it such that the topics for2D Pose Estimate
. (You do this to localize each robot individually) -
- Should always be
/namespace/topic_name
.
- Should always be
- You should relaunch the simulation because the robots will drift if given a long time of idling in the simulation. Then localize by doing the
2D Pose Estimate
.
After, we should get something like this:
Path Planning
To path plan for multiple robots, we need n path planning node systems. This means for our two robot, we need:
- two
controller_server
nodes - two
planner_server
nodes - two
recoveries_server
nodes - two
bt_navigator
nodes
- Step 1
- Step 2
- Step 3
- Add a
namespace
to the launches of the nodes.
Example below for one single node, the planner_server
. We need to do this for the 4 nodes, for each robot, meaning we should have 8 nodes in total for the path planning system.
Node(
namespace='tb3_0',
package='nav2_planner',
executable='planner_server',
name='planner_server',
output='screen',
parameters=[planner_yaml_tb1_0]
),
- Create respective config files for each robot.
- This means two of each yamls for
planner_server
,controller
,bt_navigator
,recovery
files. - Modify the content of each file and name it with good convention such as
tb3_0_planner_server.yaml
. -
- First line must contain the full new name of the node:
tb3_0/planner_server
- First line must contain the full new name of the node:
-
- All frames of the config must be modified to include the
namespace
. Theglobal_frame_id
doesn't need to since it's a single global frame.
- All frames of the config must be modified to include the
-
- Topics don't need to be modified since they're done via launch file with the argument
namespace
.
- Topics don't need to be modified since they're done via launch file with the argument
-
- The
map_topic
must be forced to/map
or else the map server will be publishing totb3_0/map
.
- The
- planner_server
- controller.yaml
- bt_navigator.yaml
- recovery.yaml
- Change
global_costmap
to include the namespace:tb3_0/global_costmap
. - Do the same for
robot_base_frame
:robot_base_frame: tb3_0/base_link
.
tb3_0/planner_server:
ros__parameters:
expected_planner_frequency: 10.0
use_sim_time: True
planner_plugins: ["GridBased"]
GridBased:
plugin: "nav2_navfn_planner/NavfnPlanner"
tolerance: 0.5
use_astar: false
allow_unknown: true
tb3_0/global_costmap:
global_costmap:
ros__parameters:
update_frequency: 1.0
publish_frequency: 1.0
global_frame: map
# This has to be set
map_topic: /map
robot_base_frame: tb3_0/base_link
use_sim_time: True
robot_radius: 0.15
resolution: 0.05
track_unknown_space: true
plugins: ["static_layer", "obstacle_layer", "inflation_layer"]
static_layer:
plugin: "nav2_costmap_2d::StaticLayer"
map_subscribe_transient_local: True
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
cost_scaling_factor: 3.0
inflation_radius: 0.35
always_send_full_costmap: True
obstacle_layer:
plugin: "nav2_costmap_2d::ObstacleLayer"
enabled: True
observation_sources: scan
scan:
# The scan is let as is because with namespace on topc it remaps properly , NO f.slash
topic: /tb3_0/scan
max_obstacle_height: 2.0
clearing: True
marking: True
data_type: "LaserScan"
raytrace_max_range: 3.0
raytrace_min_range: 0.0
obstacle_max_range: 2.5
obstacle_min_range: 0.0
- Same as planner_server
tb3_0/controller_server:
ros__parameters:
use_sim_time: True
controller_frequency: 10.0
min_x_velocity_threshold: 0.001
min_y_velocity_threshold: 0.5
min_theta_velocity_threshold: 0.001
failure_tolerance: 0.3
progress_checker_plugin: "progress_checker"
goal_checker_plugins: ["general_goal_checker"]
controller_plugins: ["FollowPath"]
# Progress checker parameters
progress_checker:
plugin: "nav2_controller::SimpleProgressChecker"
required_movement_radius: 0.5
movement_time_allowance: 10.0
# Goal checker parameters
general_goal_checker:
stateful: True
plugin: "nav2_controller::SimpleGoalChecker"
xy_goal_tolerance: 0.25
yaw_goal_tolerance: 0.25
# DWB parameters
FollowPath:
plugin: "dwb_core::DWBLocalPlanner"
debug_trajectory_details: True
min_vel_x: 0.0
min_vel_y: 0.0
max_vel_x: 0.26
max_vel_y: 0.0
max_vel_theta: 1.0
min_speed_xy: 0.0
max_speed_xy: 0.26
min_speed_theta: 0.0
acc_lim_x: 2.5
acc_lim_y: 0.0
acc_lim_theta: 3.2
decel_lim_x: -2.5
decel_lim_y: 0.0
decel_lim_theta: -3.2
vx_samples: 20
vy_samples: 5
vtheta_samples: 20
sim_time: 1.7
linear_granularity: 0.05
angular_granularity: 0.025
transform_tolerance: 0.2
xy_goal_tolerance: 0.25
trans_stopped_velocity: 0.25
short_circuit_trajectory_evaluation: True
stateful: True
critics: ["RotateToGoal", "Oscillation", "BaseObstacle", "GoalAlign", "PathAlign", "PathDist", "GoalDist"]
BaseObstacle.scale: 0.02
PathAlign.scale: 32.0
PathAlign.forward_point_distance: 0.1
GoalAlign.scale: 24.0
GoalAlign.forward_point_distance: 0.1
PathDist.scale: 32.0
GoalDist.scale: 24.0
RotateToGoal.scale: 32.0
RotateToGoal.slowing_factor: 5.0
RotateToGoal.lookahead_time: -1.0
tb3_0/local_costmap:
local_costmap:
ros\_\_parameters:
update_frequency: 5.0
publish_frequency: 2.0
global_frame: tb3_0/odom
robot_base_frame: tb3_0/base_link
use_sim_time: True
rolling_window: true
width: 1
height: 1
resolution: 0.05
robot_radius: 0.15
plugins: ["voxel_layer", "inflation_layer"]
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
cost_scaling_factor: 3.0
inflation_radius: 0.35
voxel_layer:
plugin: "nav2_costmap_2d::VoxelLayer"
enabled: True
publish_voxel_map: True
origin_z: 0.0
z_resolution: 0.05
z_voxels: 16
max_obstacle_height: 2.0
mark_threshold: 0
observation_sources: scan
scan:
topic: /tb3_0/scan
max_obstacle_height: 2.0
clearing: True
marking: True
data_type: "LaserScan"
raytrace_max_range: 3.0
raytrace_min_range: 0.0
obstacle_max_range: 2.5
obstacle_min_range: 0.0
static_layer:
map_subscribe_transient_local: True
always_send_full_costmap: True
- Set the
base_frame
to includenamespace
. - Set the
default_nav_to_pose_bt_xml
via absolute path. If you wanna do relative, you can.default_nav_to_pose_bt_xml: "/home/user/ros2_ws/src/path_planner_server/config/behavior.xml"
- Also remove the publication of behavior status of Groot or else the Groot introspection port for one
bt-navigator
will collide with the port of the other. Reference -
- To disconnect, add to eahc of the
bt-navigator
:enable_groot_monitoring: false
- To disconnect, add to eahc of the
tb3_0/bt_navigator:
ros__parameters:
use_sim_time: True
global_frame: map
robot_base_frame: tb3_0/base_link
odom_topic: tb3_0/odom
bt_loop_duration: 10
default_server_timeout: 20
default_nav_to_pose_bt_xml: "/home/user/ros2_ws/src/path_planner_server/config/behavior.xml"
enable_groot_monitoring: false
plugin_lib_names:
- nav2_compute_path_to_pose_action_bt_node
- nav2_compute_path_through_poses_action_bt_node
- nav2_smooth_path_action_bt_node
- nav2_follow_path_action_bt_node
- nav2_spin_action_bt_node
- nav2_wait_action_bt_node
- nav2_back_up_action_bt_node
- nav2_drive_on_heading_bt_node
- nav2_clear_costmap_service_bt_node
- nav2_is_stuck_condition_bt_node
- nav2_goal_reached_condition_bt_node
- nav2_goal_updated_condition_bt_node
- nav2_globally_updated_goal_condition_bt_node
- nav2_is_path_valid_condition_bt_node
- nav2_initial_pose_received_condition_bt_node
- nav2_reinitialize_global_localization_service_bt_node
- nav2_rate_controller_bt_node
- nav2_distance_controller_bt_node
- nav2_speed_controller_bt_node
- nav2_truncate_path_action_bt_node
- nav2_truncate_path_local_action_bt_node
- nav2_goal_updater_node_bt_node
- nav2_recovery_node_bt_node
- nav2_pipeline_sequence_bt_node
- nav2_round_robin_node_bt_node
- nav2_transform_available_condition_bt_node
- nav2_time_expired_condition_bt_node
- nav2_path_expiring_timer_condition
- nav2_distance_traveled_condition_bt_node
- nav2_single_trigger_bt_node
- nav2_goal_updated_controller_bt_node
- nav2_is_battery_low_condition_bt_node
- nav2_navigate_through_poses_action_bt_node
- nav2_navigate_to_pose_action_bt_node
- nav2_remove_passed_goals_action_bt_node
- nav2_planner_selector_bt_node
- nav2_controller_selector_bt_node
- nav2_goal_checker_selector_bt_node
- nav2_controller_cancel_bt_node
- nav2_path_longer_on_approach_bt_node
- nav2_wait_cancel_bt_node
- nav2_spin_cancel_bt_node
- nav2_back_up_cancel_bt_node
- nav2_drive_on_heading_cancel_bt_node
We use the same behavior.xml
for both robots.
- Same steps as first two.
tb3_0/behavior_server:
ros__parameters:
costmap_topic: local_costmap/costmap_raw
footprint_topic: local_costmap/published_footprint
cycle_frequency: 10.0
behavior_plugins: ["spin", "backup", "wait"]
spin:
plugin: "nav2_behaviors/Spin"
backup:
plugin: "nav2_behaviors/BackUp"
wait:
plugin: "nav2_behaviors/Wait"
global_frame: tb3_0/odom
robot_base_frame: tb3_0/base_link
transform_timeout: 0.1
use_sim_time: true
simulate_ahead_time: 2.0
max_rotational_vel: 1.0
min_rotational_vel: 0.4
rotational_acc_lim: 3.2
- Add the nodes to the lifecycle manager
Node(
package='nav2_lifecycle_manager',
executable='lifecycle_manager',
name='lifecycle_manager_pathplanner',
output='screen',
parameters=[{'autostart': True},
{'bond_timeout':0.0},
{'node_names': [
'tb3_0/planner_server',
'tb3_0/controller_server',
'tb3_0/recoveries_server',
'tb3_0/bt_navigator',
'tb3_1/planner_server',
'tb3_1/controller_server',
'tb3_1/recoveries_server',
'tb3_1/bt_navigator'
]}])
The final launch file should look like this.
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
controller_yaml_tb3_0 = os.path.join(get_package_share_directory('path_planner_server'), 'config', 'tb3_0_controller.yaml')
planner_yaml_tb3_0 = os.path.join(get_package_share_directory('path_planner_server'), 'config', 'tb3_0_planner_server.yaml')
recovery_yaml_tb3_0 = os.path.join(get_package_share_directory('path_planner_server'), 'config', 'tb3_0_recovery.yaml')
bt_navigator_yaml_tb3_0 = os.path.join(get_package_share_directory('path_planner_server'), 'config', 'tb3_0_bt_navigator.yaml')
controller_yaml_tb3_1 = os.path.join(get_package_share_directory('path_planner_server'), 'config', 'tb3_1_controller.yaml')
planner_yaml_tb3_1 = os.path.join(get_package_share_directory('path_planner_server'), 'config', 'tb3_1_planner_server.yaml')
recovery_yaml_tb3_1 = os.path.join(get_package_share_directory('path_planner_server'), 'config', 'tb3_1_recovery.yaml')
bt_navigator_yaml_tb3_1 = os.path.join(get_package_share_directory('path_planner_server'), 'config', 'tb3_1_bt_navigator.yaml')
return LaunchDescription([
## TB3_0
Node(
namespace='tb3_0',
package='nav2_controller',
executable='controller_server',
name='controller_server',
output='screen',
parameters=[controller_yaml_tb3_0]),
Node(
namespace='tb3_0',
package='nav2_planner',
executable='planner_server',
name='planner_server',
output='screen',
parameters=[planner_yaml_tb3_0]),
Node(
namespace='tb3_0',
package='nav2_behaviors',
executable='behavior_server',
name='recoveries_server',
parameters=[recovery_yaml_tb3_0],
output='screen'),
Node(
namespace='tb3_0',
package='nav2_bt_navigator',
executable='bt_navigator',
name='bt_navigator',
output='screen',
parameters=[bt_navigator_yaml_tb3_0]),
# ## TB3_1
Node(
namespace='tb3_1',
package='nav2_controller',
executable='controller_server',
name='controller_server',
output='screen',
parameters=[controller_yaml_tb3_1]),
Node(
namespace='tb3_1',
package='nav2_planner',
executable='planner_server',
name='planner_server',
output='screen',
parameters=[planner_yaml_tb3_1]),
Node(
namespace='tb3_1',
package='nav2_behaviors',
executable='behavior_server',
name='recoveries_server',
parameters=[recovery_yaml_tb3_1],
output='screen'),
Node(
namespace='tb3_1',
package='nav2_bt_navigator',
executable='bt_navigator',
name='bt_navigator',
output='screen',
parameters=[bt_navigator_yaml_tb3_1]),
## LIFECICLE MANAGER
Node(
package='nav2_lifecycle_manager',
executable='lifecycle_manager',
name='lifecycle_manager',
output='screen',
parameters=[{'autostart': True},
{'bond_timeout':0.0},
{'node_names': ['tb3_0/controller_server',
'tb3_0/planner_server',
'tb3_0/recoveries_server',
'tb3_0/bt_navigator',
'tb3_1/controller_server',
'tb3_1/planner_server',
'tb3_1/recoveries_server',
'tb3_1/bt_navigator',
]}])
])
The order is localization -> path_planning -> rviz2 for launches.
Configuring RVIZ2
- Add a
Path
display configured for the/tb3_0/plan
topic and select theLine Style
asBillboards
with Red as it's color. - Add another
Path
for the/tb3_1/plan
and choose a different color for it. - Add a
Map
display and configure the topic for the global costmap:/tb3_0/global_costmap/costmap
- Add another
Map
for/tb3_1/global_costmap/costmap
. - Add another
Map
for the local costmap for again each of the topics:/tb_3_0/local_costmap/costmap
and/tb3_1/local_costmap/costmap
. - All the
cost_maps
change the color scheme frommap
tocost_map
to see the nice colors. - To make the robot move, use the
Tool Properties
and change the2D Goal Pose
to be/tb3_0/goal_pose
and then use the2D Goal Pose
. For the other robot, use the other namespace/tb3_1/goal_pose
.
Single Launch
To make this simpler, we can create several launch files to concentrate everything into a single launch. Inside of our path_planner_server
, we can create the launch multi_main.launch.xml
. Inside the cartographer_slam
, we also create the launch file start_rviz.launch.py
which'll launch RVIZ2
without terminal commands. Both of these files should be inside of the launch
directory.
- multi_main.launch.xml
- start_rviz.launch.py
<?xml version='1.0' ?>
<launch>
<include file="$(find-pkg-share localization)/launch/multi_localization.launch.py"/>
<include file="$(find-pkg-share path_planner_server)/launch/multi_pathplanner.launch.py"/>
<include file="$(find-pkg-share cartographer_slam)/launch/start_rviz.launch.py">
<arg name="use_sim_time" value="true"/>
<arg name="rviz_config_dir" value="$(find-pkg-share cartographer_slam)/rviz/tb3_1_and_2.rviz" />
</include>
</launch>
import os
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
use_sim_time = LaunchConfiguration('use_sim_time', default='false')
rviz_config_dir = LaunchConfiguration("rviz_config_dir")
return LaunchDescription([
DeclareLaunchArgument(
"rviz_config_dir", default_value="", description="TRviz"),
DeclareLaunchArgument(
'use_sim_time',
default_value='false',
description='Use simulation (Gazebo) clock if true'),
Node(
package='rviz2',
executable='rviz2',
name='rviz2',
arguments=['-d', rviz_config_dir],
parameters=[{'use_sim_time': use_sim_time}],
output='screen'),
])
Then launch via:
ros2 launch path_planner_server multi_main.launch.xml