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, theodom→base_footprinttransform, and/odom.Your sensors: the same
LaserScanorPointCloud2topics.Your maps: the
.yaml+.pgmpairs ofmap_serverand 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_timeand 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 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_freqis the rate of the part of a component that runs in the real-time control cycle (localization prediction, control), andfreqthe rate of the rest (map updates, planning, AMCL correction).Topics: velocities go out on
/cmd_vel(geometry_msgs/Twist). If your base expectsTwistStamped(Nav2’senable_stamped_cmd_vel), setcontroller_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 withcompute_odom_from_tf: true.Other controllers: for MPPI, start from
costmap.mppi.params.yamlin easynav_indoor_testcase. Every plugin’s parameters are in its README (see EasyNav Plugins).Recoveries (
recovery_node) do what Nav2’s recovery subtrees,behavior_serverandcollision_monitordo: 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_missionaborts the mission, so your application gets a failure, as a Nav2 client getsABORTED. 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 (
ObstacleTooCloseEvaluatorwithSafeRetreatRecovery), a lost localization (AmclConvergenceEvaluatorwithAmclRelocalizeMitigation), waiting for a human, or terminating EasyNav on a miswired ROS graph. See Recovery System and therecovery_nodesection ofcostmap.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
NavigateToPosenode, 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 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)orsend_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 ofNAVIGATION_FINISHED,NAVIGATION_FAILED,NAVIGATION_REJECTED,NAVIGATION_CANCELLEDorERROR;get_feedback()andget_result()give the details (current pose, navigation time, straight-line distance to the goal; the reason of a failure instatus_message);cancel(),pause()andresume();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
obstaclesfilter. 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_timeas 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; checkrobot_frameand theodom→ 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 listshows/navigate_to_pose).
Next steps
Getting Started: a first run in simulation, if you want to see EasyNav working before touching your robot.
Navigating with the Costmap Stack: the same stack, step by step, in simulation.
EasyNav Plugins: every plugin and its parameters.
Core Design and Architecture: how EasyNav works inside.