Migration Guide

If your robot already navigates with Nav2, trying EasyNav on it takes little work: both build on the same ROS 2 conventions. This guide shows what you can reuse, how Nav2’s components map to EasyNav’s, how to write the parameter file and how your applications send goals. Both can live side by side: EasyNav does not change your robot, your maps or your TF tree.

What stays the same

Most of what you built for Nav2 is reused as is:

  • Your robot: URDF, robot_state_publisher, drivers, the odom → base_footprint transform, and /odom.

  • Your sensors: the same LaserScan or PointCloud2 topics.

  • Your maps: the .yaml + .pgm pairs of map_server and SLAM Toolbox load directly.

  • Your frames: map, odom, base_link, base_footprint (REP-105) by default.

  • Your tools: RViz2, its 2D Pose Estimate and 2D Goal Pose buttons, SLAM Toolbox, use_sim_time and your simulator.

  • Your Nav2 clients, through the Nav2 bridge (see Step 5: Send goals from your applications).

What changes is how navigation runs: one process (system_main) with one parameter file, where each component loads the plugin you choose.

Step 1: Install EasyNav

Follow Build & Install. Besides the core (ros-<distro>-easynav), install the plugins of the configuration in this guide, the closest to a typical Nav2 setup:

sudo apt install \
  ros-<distro>-easynav-costmap-maps-manager \
  ros-<distro>-easynav-costmap-localizer \
  ros-<distro>-easynav-costmap-planner \
  ros-<distro>-easynav-regulated-pp-controller \
  ros-<distro>-easynav-diagnostic-recovery \
  ros-<distro>-easynav-collision-safety-reflex \
  ros-<distro>-easynav-no-path-evaluator \
  ros-<distro>-easynav-controller-stuck-evaluator \
  ros-<distro>-easynav-advance-recovery \
  ros-<distro>-easynav-cancel-mission-recovery

The last six are the recovery system (see Step 3). Some plugins are not packaged for every distribution yet (see the note in Build & Install). If one is missing, use Pixi or build easynav_plugins from source.

Let EasyNav run in real time. This is a one-time system setting your Nav2 setup may not have needed. EasyNav runs its control cycle with real-time priority (SCHED_FIFO 80), so that a loaded computer does not delay the commands to the robot. Linux only allows it if your user may use that priority. Check it:

ulimit -r   # must be 80 or more

If it is lower (Ubuntu’s default is 0), follow Real-time system setup (optional): a one-time change for your user, a systemd service or a Docker container, then log in again. Without it EasyNav still runs, with normal priority, and warns at startup: Failed to set Real Time (...). Running with normal priority.

Step 2: Your map

If you already have a Nav2 map, use it as it is. Put the .yaml and the image in a package of your workspace, e.g. my_robot_maps/maps/office.yaml, or anywhere and give an absolute path (see Step 3).

If you need a new one, map exactly as you would for Nav2: SLAM Toolbox, teleoperation, and map_saver or slam_toolbox/save_map. Mapping with SLAM Toolbox and EasyNav walks through it step by step in simulation, and Mapping with the Costmap Stack shows how to check the result with the Costmap maps manager.

Step 3: The parameter file

A single YAML file configures everything, one section per node. Each node lists the plugin it uses in <kind>_types (one entry), and that entry holds the plugin and its parameters. This is a complete file for a robot with a 2D lidar, Nav2-style: AMCL, A* on a costmap and Regulated Pure Pursuit. It is easynav_indoor_testcase/robots_params/costmap.rpp.params.yaml, simplified. The comments say where each value comes from in your Nav2 file.

system_node:
  ros__parameters:
    use_sim_time: true
    # costmap robot_radius / footprint
    robot_geometry:
      radius: 0.3             # circumscribed radius (m)
      inscribed_radius: 0.25  # largest circle inside the robot (m)
      height: 0.5
    # goal_checker xy_goal_tolerance / yaw_goal_tolerance: when a goal is reached
    position_tolerance: 0.3
    angle_tolerance: 0.15
    use_real_time: true       # the default; see "Let EasyNav run in real time" in Step 1
    # amcl base_frame_id, odom_frame_id, global_frame_id (these are the defaults)
    robot_frame: base_link
    odom_frame: odom
    map_frame: map

sensors_node:
  ros__parameters:
    use_sim_time: true
    # costmap observation_sources, and amcl scan_topic
    sensors: [laser1]
    laser1:
      topic: /scan
      type: sensor_msgs/msg/LaserScan

maps_manager_node:
  ros__parameters:
    use_sim_time: true
    map_types: [costmap]
    costmap:
      plugin: easynav_costmap_maps_manager/CostmapMapsManager
      freq: 10.0
      # map_server yaml_filename: a package and a path inside it, or an absolute path alone
      package: my_robot_maps
      map_path_file: maps/office.yaml
      # costmap plugins: obstacle_layer and inflation_layer
      filters: [obstacles, inflation]
      obstacles:
        plugin: easynav_costmap_maps_manager/CostmapMapsManager/ObstaclesFilter
      inflation:
        plugin: easynav_costmap_maps_manager/CostmapMapsManager/InflationFilter
        inflation_radius: 0.8
        cost_scaling_factor: 3.0

localizer_node:
  ros__parameters:
    use_sim_time: true
    localizer_types: [amcl]
    amcl:
      plugin: easynav_costmap_localizer/AMCLLocalizer
      rt_freq: 50.0
      freq: 5.0
      reseed_freq: 1.0
      num_particles: 100        # amcl max_particles
      # amcl set_initial_pose / initial_pose (or use RViz's 2D Pose Estimate)
      initial_pose:
        x: 0.0
        y: 0.0
        yaw: 0.0
        std_dev_xy: 0.1
        std_dev_yaw: 0.01

planner_node:
  ros__parameters:
    use_sim_time: true
    planner_types: [costmap]
    costmap:
      plugin: easynav_costmap_planner/CostmapPlanner
      freq: 0.5                 # planner_server expected_planner_frequency
      continuous_replan: true   # replan while navigating, as Nav2's default tree does
      cost_factor: 10.0

controller_node:
  ros__parameters:
    use_sim_time: true
    # velocity_smoother max_velocity / max_accel / max_decel, and RPP desired_linear_vel
    robot_limits:
      max_linear_vel: 0.5
      min_linear_vel: -0.2      # 0: never backwards
      max_angular_vel: 1.0
      max_linear_acc: 1.0
      max_linear_decel: 1.0
      max_angular_acc: 2.0
      max_angular_decel: 2.0
    controller_types: [rpp]
    rpp:
      plugin: easynav_regulated_pp_controller/RegulatedPurePursuitController
      rt_freq: 30.0             # controller_server controller_frequency
      # From here on, the same names as Nav2's RPP
      lookahead_dist: 0.5
      min_lookahead_dist: 0.3
      max_lookahead_dist: 0.9
      lookahead_time: 1.2
      use_velocity_scaled_lookahead_dist: true
      use_rotate_to_heading: true
      rotate_to_heading_min_angle: 0.785
      use_regulated_linear_velocity_scaling: true
      regulated_linear_scaling_min_radius: 0.9
      regulated_linear_scaling_min_speed: 0.15
      allow_reversing: false
      xy_goal_tolerance: 0.1
      yaw_goal_tolerance: 0.105

# bt_navigator's recovery subtrees, behavior_server and collision_monitor, with the plugins'
# default parameters
recovery_node:
  ros__parameters:
    use_sim_time: true
    recovery_manager:
      plugin: easynav_diagnostic_recovery/DiagnosticRecoveryManager
      # Brakes, every control cycle, if the command would hit an obstacle
      safety_reflex_types: [collision]
      collision:
        plugin: easynav_collision_safety_reflex/CollisionSafetyReflex
      # Diagnose problems...
      evaluator_types: [no_path, controller_stuck]
      no_path:
        plugin: easynav_no_path_evaluator/NoPathEvaluator
      controller_stuck:
        plugin: easynav_controller_stuck_evaluator/ControllerStuckEvaluator
      # ...and fix them, by priority (lower first); the last resort aborts the mission
      mitigation_types: [advance, cancel_mission]
      advance:
        plugin: easynav_advance_recovery/AdvanceRecovery
      cancel_mission:
        plugin: easynav_cancel_mission_recovery/CancelMissionRecovery
        priority: 2000

A few things are worth knowing when you translate your own file:

  • Velocity limits live in one place, controller_node.robot_limits, not in each controller. Every command published respects them, whatever produced it.

  • Frequencies: rt_freq is the rate of the part of a component that runs in the real-time control cycle (localization prediction, control), and freq the rate of the rest (map updates, planning, AMCL correction).

  • Topics: velocities go out on /cmd_vel (geometry_msgs/Twist). If your base expects TwistStamped (Nav2’s enable_stamped_cmd_vel), set controller_node.use_cmd_vel_stamped: true: they then go out on /cmd_vel_stamped, so remap it to what your base listens to. AMCL reads odometry from /odom, or from TF with compute_odom_from_tf: true.

  • Other controllers: for MPPI, start from costmap.mppi.params.yaml in easynav_indoor_testcase. Every plugin’s parameters are in its README (see EasyNav Plugins).

  • Recoveries (recovery_node) do what Nav2’s recovery subtrees, behavior_server and collision_monitor do: the collision reflex brakes before a collision; evaluators diagnose problems (here, no path to the goal and a robot that does not progress); mitigations fix them (here, advancing a little). When nothing can, cancel_mission aborts the mission, so your application gets a failure, as a Nav2 client gets ABORTED. Keep this section: without it, nothing brakes before a collision, and a mission never fails, it waits forever.

  • More recoveries, when you need them: an obstacle too close (ObstacleTooCloseEvaluator with SafeRetreatRecovery), a lost localization (AmclConvergenceEvaluator with AmclRelocalizeMitigation), waiting for a human, or terminating EasyNav on a miswired ROS graph. See Recovery System and the recovery_node section of costmap.rpp.params.yaml.

Step 4: Run it

Start your robot or simulator as usual, then EasyNav, instead of nav2_bringup:

ros2 run easynav_system system_main --ros-args --params-file my_robot_easynav.yaml

or, from a launch file:

from launch import LaunchDescription
from launch.actions import Shutdown
from launch_ros.actions import Node


def generate_launch_description():
    return LaunchDescription([
        Node(
            package='easynav_system',
            executable='system_main',
            parameters=['/path/to/my_robot_easynav.yaml'],
            output='screen',
            on_exit=Shutdown(reason='EasyNav exited'),
        ),
    ])

In RViz2:

  • add a Map display on /maps_manager_node/costmap/map (static map) or /maps_manager_node/costmap/dynamic_map (with obstacles and inflation). Set the static map’s QoS durability to Transient Local;

  • add a Path display on /planner_node/costmap/path;

  • give the initial pose with 2D Pose Estimate, as with Nav2;

  • send a goal with 2D Goal Pose.

To see what is going on inside (the navigation state, each plugin’s timing, diagnostics), run the terminal dashboard in another terminal:

ros2 run easynav_tools tui

and see ros2 easynav — EasyNav CLI Extensions for the ros2 easynav commands.

Step 5: Send goals from your applications

Choose by what you already have:

  • Code that uses Nav2’s action (the Simple Commander, a BT NavigateToPose node, the Nav2 RViz panel, ros2 action send_goal): use the Nav2 bridge. You change nothing in them.

  • New code, or code you are willing to adapt: use GoalManagerClient. It is EasyNav’s own interface, and it also does several goals in one request, pause and resume.

Option A: the Nav2 bridge

easynav_nav2_bridge is a nav2_msgs/action/NavigateToPose server on navigate_to_pose that forwards goals to EasyNav, and EasyNav’s feedback and result back, so Nav2 clients work with EasyNav unchanged.

It is built from source, in the workspace where you have EasyNav:

cd ~/easynav_ws/src
git clone https://github.com/EasyNavigation/easynav_nav2_bridge.git
cd ~/easynav_ws
rosdep install --from-paths src --ignore-src -y -r
colcon build --symlink-install --packages-select easynav_nav2_bridge
source install/setup.bash

Run it next to EasyNav (or add it to your launch file):

ros2 run easynav_nav2_bridge nav2_bridge_main

and send goals as you did to Nav2:

ros2 action send_goal /navigate_to_pose nav2_msgs/action/NavigateToPose \
  "{pose: {header: {frame_id: map}, pose: {position: {x: 2.0, y: 1.0}, orientation: {w: 1.0}}}}" \
  --feedback

or with the Simple Commander:

from nav2_simple_commander.robot_navigator import BasicNavigator, TaskResult

navigator = BasicNavigator()
# Not navigator.waitUntilNav2Active(): it waits for Nav2's lifecycle nodes (amcl,
# bt_navigator), which do not exist with EasyNav.
navigator.goToPose(goal)   # a PoseStamped in the map frame
while not navigator.isTaskComplete():
    feedback = navigator.getFeedback()
if navigator.getResult() == TaskResult.SUCCEEDED:
    print('Goal reached')

What to expect:

  • Feedback: current_pose, navigation_time and distance_remaining (straight-line distance to the goal), 10 times per second (parameter feedback_rate). estimated_time_remaining is not computed yet: it stays at zero.

  • Result: SUCCEEDED when EasyNav reaches the goal; ABORTED if it fails (the reason is logged by the bridge); CANCELED when the client cancels.

  • A new goal preempts the current one, as with bt_navigator.

  • Only NavigateToPose: the goal’s behavior_tree field is ignored, and there is no NavigateThroughPoses (use GoalManagerClient::send_goals()).

Option B: GoalManagerClient

GoalManagerClient talks to EasyNav on the easynav_control topic (see Sending Navigation Commands to EasyNav). It is a small state machine you drive from your node, usually from a timer:

  • send_goal(pose) or send_goals(goals) (several, in order) start a navigation. Calling them again while navigating replaces the goals;

  • get_state() tells how it goes: SENT_GOAL, ACCEPTED_AND_NAVIGATING, and then one of NAVIGATION_FINISHED, NAVIGATION_FAILED, NAVIGATION_REJECTED, NAVIGATION_CANCELLED or ERROR;

  • get_feedback() and get_result() give the details (current pose, navigation time, straight-line distance to the goal; the reason of a failure in status_message);

  • cancel(), pause() and resume();

  • after a final state, reset() before the next goal.

In Python (package easynav_support_py):

import rclpy
from rclpy.node import Node
from geometry_msgs.msg import PoseStamped
from easynav_goalmanager_py import GoalManagerClient, ClientState


class GoToKitchen(Node):

    def __init__(self):
        super().__init__('go_to_kitchen')
        self.client = GoalManagerClient(self)
        self.sent = False
        self.timer = self.create_timer(0.2, self.cycle)

    def cycle(self):
        state = self.client.get_state()
        if not self.sent:
            goal = PoseStamped()
            goal.header.frame_id = 'map'
            goal.header.stamp = self.get_clock().now().to_msg()
            goal.pose.position.x = 2.0
            goal.pose.orientation.w = 1.0
            self.client.send_goal(goal)
            self.sent = True
        elif state == ClientState.ACCEPTED_AND_NAVIGATING:
            fb = self.client.get_feedback()
            self.get_logger().info(f'{fb.distance_to_goal:.2f} m to go')
        elif state == ClientState.NAVIGATION_FINISHED:
            self.get_logger().info('Arrived')
            self.client.reset()
            self.timer.cancel()
        elif state in (ClientState.NAVIGATION_FAILED, ClientState.NAVIGATION_REJECTED,
                       ClientState.NAVIGATION_CANCELLED, ClientState.ERROR):
            self.get_logger().error(self.client.get_result().status_message)
            self.client.reset()
            self.timer.cancel()


rclpy.init()
rclpy.spin(GoToKitchen())

In C++ (easynav_system/GoalManagerClient.hpp), the same, with the states in easynav::GoalManagerClient::State:

#include "easynav_system/GoalManagerClient.hpp"

// In your node's constructor:
client_ = easynav::GoalManagerClient::make_shared(shared_from_this());

// In a timer callback:
using State = easynav::GoalManagerClient::State;
switch (client_->get_state()) {
  case State::IDLE:
    client_->send_goal(goal);  // geometry_msgs::msg::PoseStamped, frame "map"
    break;
  case State::NAVIGATION_FINISHED:
  case State::NAVIGATION_FAILED:
  case State::NAVIGATION_REJECTED:
  case State::NAVIGATION_CANCELLED:
  case State::ERROR:
    RCLCPP_INFO(get_logger(), "%s", client_->get_result().status_message.c_str());
    client_->reset();
    break;
  default:
    break;
}

shared_from_this() is not available in a constructor: create the client in an init() method called after the node is created, or in the first timer callback. For a full example with several waypoints, see Patrolling Behavior (C++ and Python).

Option C: just a pose

EasyNav also listens to /goal_pose (geometry_msgs/PoseStamped), which is what RViz’s 2D Goal Pose publishes. It is the quickest way to try, without feedback or result.

Differences to keep in mind

  • Mission logic lives outside. EasyNav plans, follows, replans and recovers by itself, without a behavior tree. What you did with a custom tree on top (sequences of goals, tasks between them, conditions) goes in a node of yours with GoalManagerClient, or in your own behavior tree with its own action nodes.

  • One costmap. The controller and the planner share the same map, updated with live sensor data by the obstacles filter. Controllers such as RPP also slow down near obstacles using the sensors directly.

  • Real-time control cycle. Localization prediction, control and collision checking run in a real-time thread, at rt_freq; the rest runs apart, so a slow planner or map update never delays control. It needs the system set up for it once (Real-time system setup (optional)). See Core Design and Architecture.

  • Stopping is automatic. If the controller stops producing commands, the robot brakes to zero after controller_node.cmd_timeout (0.5 s); when EasyNav is stopped (Ctrl+C included) it brakes within its deceleration limits and leaves a zero command.

  • Failed missions. With the recovery system above, a problem its mitigations cannot fix aborts the mission: the client gets the reason (status_message), and through the Nav2 bridge, ABORTED. EasyNav keeps running, ready for the next goal.

  • Safety-rated setups (safety PLC, safety scanner) have their own page: Safety.

Troubleshooting

  • EasyNav exits right after starting: a parameter is wrong or a plugin is not installed. The message just before it exits names it.

  • The robot does not move: check that every node has the same use_sim_time as the simulator, that your base listens to /cmd_vel (or /cmd_vel_stamped, see Step 3), and that the robot is localized (set the initial pose).

  • “Failed to set Real Time … Running with normal priority”: your user may not use real-time priority; see Real-time system setup (optional). It works anyway, but control may be delayed when the computer is loaded.

  • The map does not show up in RViz2: set the Map display’s durability to Transient Local.

  • AMCL does not converge: give the initial pose with 2D Pose Estimate, or set initial_pose; check robot_frame and the odom → base transform.

  • A Nav2 client waits forever: do not wait for Nav2’s lifecycle nodes (e.g. waitUntilNav2Active()), and check the bridge is running (ros2 action list shows /navigate_to_pose).

Next steps