Adding a Controller from Nav2

As stated previously, the Controller is the magic that gets a robot to move, and governs a lot of the behaviour of how it navigates. It is also the most fiddly thing that will require tuning on a real robot to work reliably.

For a full list of Controllers available, visit the Nav 2 Controller Documentation.

The Controller

The Controller monitors the robot’s state (velocity, pose), the given path, and sensor measurements to compute velocity commands that lead to progress along the path.

Once again we have list of “plugins” to choose from, allowing us flexibility over what controller architecture to use. In fact, you can write your own controller (and planner)!

For this task we will stick with DWB planner, which is the general default choice.

Note

The name DWB is a inside joke made by David Lu. The algorithm used is called “Dynamic Window Approach” (DWA), however, as it was rewritten from ROS 1 to ROS 2, the author thought themselves hilarious to call it DWB (as in, “B” comes after “A”).

Note

The original DWA approach is based on the paper: Fox, D., Burgard, W. and Thrun, S. (1997). The Dynamic Window Approach to Collision Avoidance.

I told you that all these packages are based on research papers.

As mentioned, these Controllers can be tuned or altered to provide the behaviour you desire (e.g. how faithfully should it stick to the global path). It is not possible to exhaustively cover this tuning for every controller here, but most provide a tuning guide of sorts. For generic advice, please take a look at

Writing the Controller Config File

The format of the configuration is taken from the controller documentation. It consists of general controller parameters, but controller plugin specific parameters can also be passed. For DWB some of these parameters can be found here. An example config file for the DWB controller would look like the file below. Copy this example into a file called controller.yaml in the config directory.

 1controller_server:
 2  ros__parameters:
 3    use_sim_time: True
 4    controller_frequency: 20.0
 5    min_x_velocity_threshold: 0.001
 6    min_y_velocity_threshold: 0.5
 7    min_theta_velocity_threshold: 0.001
 8    failure_tolerance: 0.3
 9    odom_topic: "odom"
10    progress_checker_plugins: ["progress_checker"] # progress_checker_plugin: "progress_checker" For Humble and older
11    goal_checker_plugin: "goal_checker"
12    controller_plugins: ["FollowPath"] # This is where we define the DWB controller plugin
13    progress_checker:
14      plugin: "nav2_controller::SimpleProgressChecker"
15      required_movement_radius: 0.5
16      movement_time_allowance: 10.0
17    goal_checker:
18      plugin: "nav2_controller::SimpleGoalChecker"
19      xy_goal_tolerance: 0.25
20      yaw_goal_tolerance: 0.25
21      stateful: True
22    FollowPath:
23      plugin: "dwb_core::DWBLocalPlanner"
24      debug_trajectory_details: True
25      min_vel_x: 0.0
26      min_vel_y: 0.0
27      max_vel_x: 0.26
28      max_vel_y: 0.0
29      max_vel_theta: 1.0
30      min_speed_xy: 0.0
31      max_speed_xy: 0.26
32      min_speed_theta: 0.0
33      acc_lim_x: 2.5
34      acc_lim_y: 0.0
35      acc_lim_theta: 3.2
36      decel_lim_x: -2.5
37      decel_lim_y: 0.0
38      decel_lim_theta: -3.2
39      vx_samples: 20
40      vy_samples: 5
41      vtheta_samples: 20
42      sim_time: 1.7
43      linear_granularity: 0.05
44      angular_granularity: 0.025
45      transform_tolerance: 0.2
46      xy_goal_tolerance: 0.25
47      trans_stopped_velocity: 0.25
48      short_circuit_trajectory_evaluation: True
49      stateful: True
50      critics: ["RotateToGoal", "Oscillation", "BaseObstacle", "GoalAlign", "PathAlign", "PathDist", "GoalDist"]
51      BaseObstacle.scale: 0.02
52      PathAlign.scale: 32.0
53      PathAlign.forward_point_distance: 0.1
54      GoalAlign.scale: 24.0
55      GoalAlign.forward_point_distance: 0.1
56      PathDist.scale: 32.0
57      GoalDist.scale: 24.0
58      RotateToGoal.scale: 32.0
59      RotateToGoal.slowing_factor: 5.0
60      RotateToGoal.lookahead_time: -1.0

The DWB planner has many options to tune the system. It is recommended that for your own robot, you would carefully read about all the different options and see which need to be altered.

Notice as well we needed to include a goal_checker and a progress_checker, these do pretty much what you would expect. By having these separate (rather than having them included in the controller plugin), it again allows for modularity. These can be tuned as well, for example, in the goal checker we set the tolerance of how close the robot must be to the goal (as it is nearly impossible for a robot to drive exactly to the point asked of it).

Writing the Local Costmap Config File

The Local costmap in comparison to the global costmap, only covers a small portion around the robot (which is quicker to compute), and is updated more regularly. In this case, we do not necessarily need the static map, just obstacles and other threats we wish to track. Because we are using layered costmaps, it is possible to add obstacle (or other types) of layers representing different sensors. We’ll just add the lidar for obstacles this time.

Again we include the inflation layer to convert obstacles into the configuration space of the robot. Edit the controller.yaml file where the DWB parameters were added earlier to match the example below.

 1controller_server:
 2  ros__parameters:
 3    use_sim_time: True
 4    controller_frequency: 20.0
 5    min_x_velocity_threshold: 0.001
 6    min_y_velocity_threshold: 0.5
 7    min_theta_velocity_threshold: 0.001
 8    failure_tolerance: 0.3
 9    odom_topic: "odom"
10    progress_checker_plugins: ["progress_checker"] # progress_checker_plugin: "progress_checker" For Humble and older
11    goal_checker_plugin: "goal_checker"
12    controller_plugins: ["FollowPath"] # This is where we define the DWB controller plugin
13    progress_checker:
14      plugin: "nav2_controller::SimpleProgressChecker"
15      required_movement_radius: 0.5
16      movement_time_allowance: 10.0
17    goal_checker:
18      plugin: "nav2_controller::SimpleGoalChecker"
19      xy_goal_tolerance: 0.25
20      yaw_goal_tolerance: 0.25
21      stateful: True
22    FollowPath:
23      plugin: "dwb_core::DWBLocalPlanner"
24      debug_trajectory_details: True
25      min_vel_x: 0.0
26      min_vel_y: 0.0
27      max_vel_x: 0.26
28      max_vel_y: 0.0
29      max_vel_theta: 1.0
30      min_speed_xy: 0.0
31      max_speed_xy: 0.26
32      min_speed_theta: 0.0
33      acc_lim_x: 2.5
34      acc_lim_y: 0.0
35      acc_lim_theta: 3.2
36      decel_lim_x: -2.5
37      decel_lim_y: 0.0
38      decel_lim_theta: -3.2
39      vx_samples: 20
40      vy_samples: 5
41      vtheta_samples: 20
42      sim_time: 1.7
43      linear_granularity: 0.05
44      angular_granularity: 0.025
45      transform_tolerance: 0.2
46      xy_goal_tolerance: 0.25
47      trans_stopped_velocity: 0.25
48      short_circuit_trajectory_evaluation: True
49      stateful: True
50      critics: ["RotateToGoal", "Oscillation", "BaseObstacle", "GoalAlign", "PathAlign", "PathDist", "GoalDist"]
51      BaseObstacle.scale: 0.02
52      PathAlign.scale: 32.0
53      PathAlign.forward_point_distance: 0.1
54      GoalAlign.scale: 24.0
55      GoalAlign.forward_point_distance: 0.1
56      PathDist.scale: 32.0
57      GoalDist.scale: 24.0
58      RotateToGoal.scale: 32.0
59      RotateToGoal.slowing_factor: 5.0
60      RotateToGoal.lookahead_time: -1.0
61
62
63local_costmap:
64  local_costmap:
65    ros__parameters:
66      update_frequency: 5.0
67      publish_frequency: 2.0
68      global_frame: odom
69      robot_base_frame: base_link
70      use_sim_time: True
71      rolling_window: True
72      width: 3
73      height: 3
74      resolution: 0.06
75      robot_radius: 0.175
76      plugins: ["obstacle_layer", "inflation_layer"]
77      always_send_full_costmap: True
78      inflation_layer:
79        plugin: "nav2_costmap_2d::InflationLayer"
80        cost_scaling_factor: 4.0
81        inflation_radius: 0.45
82      obstacle_layer:
83        plugin: "nav2_costmap_2d::ObstacleLayer"
84        enabled: True
85        observation_sources: scan_source
86        scan_source:
87          topic: /scan
88          max_obstacle_height: 2.0
89          clearing: True
90          marking: True
91          data_type: "LaserScan"
92          raytrace_max_range: 3.0
93          raytrace_min_range: 0.0
94          obstacle_max_range: 2.5
95          obstacle_min_range: 0.0

Adding a Controller to a Launch File

Finally, we need to add the node to the launch file, along with including the config file and ensuring the lifecycle manager knows to handle it. Open the nav_demo.launch.py file and add the following lines.

  1from ament_index_python.packages import get_package_share_directory
  2from launch import LaunchDescription
  3from launch.actions import IncludeLaunchDescription
  4from launch_ros.actions import SetParameter, Node
  5from launch.launch_description_sources import PythonLaunchDescriptionSource
  6from launch.substitutions import PathJoinSubstitution
  7
  8
  9def generate_launch_description():
 10    ld = LaunchDescription()
 11
 12    # Parameters, Nodes and Launch files go here
 13
 14    # Declare package directory
 15    pkg_nav_demos = get_package_share_directory('navigation_demos')
 16    # Necessary fixes
 17    remappings = [('/tf', 'tf'), ('/tf_static', 'tf_static')]
 18
 19    lifecycle_nodes = [
 20        'controller_server',
 21        'planner_server',
 22        'behaviour_server',
 23        'bt_navigator',
 24    ]
 25
 26    # LOAD PARAMETERS FROM YAML FILES
 27    config_bt_nav     = PathJoinSubstitution([pkg_nav_demos, 'config', 'bt_nav.yaml'])
 28    config_planner    = PathJoinSubstitution([pkg_nav_demos, 'config', 'planner.yaml'])
 29    config_controller = PathJoinSubstitution([pkg_nav_demos, 'config', 'controller.yaml'])
 30
 31    # Include Gazebo Simulation
 32    launch_gazebo = IncludeLaunchDescription(
 33    PythonLaunchDescriptionSource([get_package_share_directory('gz_example_robot_description'), '/launch', '/sim_robot.launch.py']),
 34    launch_arguments={}.items(),
 35    )
 36
 37    # Include SLAM Toolbox standard launch file
 38    launch_slamtoolbox = IncludeLaunchDescription(
 39    PythonLaunchDescriptionSource([get_package_share_directory('slam_toolbox'), '/launch', '/online_async_launch.py']),
 40    launch_arguments={}.items(),
 41    )
 42
 43    # Behaviour Tree Navigator
 44    node_bt_nav = Node(
 45        package='nav2_bt_navigator',
 46        executable='bt_navigator',
 47        name='bt_navigator',
 48        output='screen',
 49        parameters=[config_bt_nav],
 50        remappings=remappings,
 51    )
 52
 53    # Behaviour Tree Server
 54    node_behaviour = Node(
 55        package='nav2_behaviors',
 56        executable='behavior_server',
 57        name='behaviour_server',
 58        output='screen',
 59        parameters=[config_bt_nav],
 60        remappings=remappings,
 61    )
 62
 63    # Planner Server Node
 64    node_planner = Node(
 65        package='nav2_planner',
 66        executable='planner_server',
 67        name='planner_server',
 68        output='screen',
 69        parameters=[config_planner],
 70        remappings=remappings,
 71    )
 72
 73    # Controller Server Node
 74    node_controller = Node(
 75        package='nav2_controller',
 76        executable='controller_server',
 77        name='controller_server',
 78        output='screen',
 79        parameters=[config_controller],
 80        remappings=remappings,
 81    )
 82
 83    # Lifecycle Node Manager to automatically start lifecycles nodes (from list)
 84    node_lifecycle_manager = Node(
 85        package='nav2_lifecycle_manager',
 86        executable='lifecycle_manager',
 87        name='lifecycle_manager_navigation',
 88        output='screen',
 89        parameters=[{'autostart': True}, {'node_names': lifecycle_nodes}],
 90    )
 91
 92
 93    # Add actions to LaunchDescription
 94    ld.add_action(SetParameter(name='use_sim_time', value=True))
 95    ld.add_action(launch_gazebo)
 96    ld.add_action(launch_slamtoolbox)
 97    ld.add_action(node_bt_nav)
 98    ld.add_action(node_behaviour)
 99    ld.add_action(node_planner)
100    ld.add_action(node_controller)
101    ld.add_action(node_lifecycle_manager)
102
103    return ld

Perform the usual colcon build, source install/setup.bash and check the launch file runs.