Autonomous Navigation Stack | Robotics Development Hub

Navigation 2 (Nav2) is the ROS 2 standard for autonomous mobile robot navigation. This guide provides a deep dive into Nav2 architecture: behavior trees, costmap layers, recovery behaviors, controller plugins, and advanced customization for dynamic environments. 1. Nav2 Architecture Overview 1.1 Core Components # Nav2 node graph # /bt_navigator - Behavior Tree executor (high-level logic) # /planner_server - Global planner (A*, Dijkstra, Smac, Theta*) # /controller_server - Local controller (DWB, TEB, MPPI, RPP) # /behavior_server - Recovery behaviors (spin, backup, wait) # /waypoint_follower - Waypoint following action # /global_costmap - Global costmap (static + inflation) # /local_costmap - Local costmap (rolling window) # /map_server - Map provider (static, costmap filters) # /lifecycle_manager - Manages lifecycle states 1.2 Lifecycle Management # All Nav2 nodes are lifecycle nodes # States: unconfigured → inactive → active → finalized # Bringup launch manages transitions # Manual CLI control ros2 lifecycle set /planner_server configure ros2 lifecycle set /planner_server activate 2. Behavior Trees (BT Navigator) 2.1 Default NavigateToPose BT 2.2 Custom Behavior Tree Nodes # Custom BT node (C++) #include class CheckBattery : public BT::ConditionNode { public: CheckBattery(const std::string& name, const BT::NodeConfiguration& config) : BT::ConditionNode(name, config) {} static BT::PortsList providedPorts() { return { BT::InputPort ("threshold", 20.0) }; } BT::NodeStatus tick() override { double threshold; getInput("threshold", threshold); double battery = getBatteryLevel(); // Your implementation return (battery > threshold) ? BT::NodeStatus::SUCCESS : BT::NodeStatus::FAILURE; } }; // Register BT::RegisterRosNode ("CheckBattery"); 2.3 BT Navigator Configuration # bt_navigator params bt_navigator: ros__parameters: use_sim_time: true global_frame: map robot_base_frame: base_link odom_topic: /odom bt_loop_duration: 10 # ms default_server_timeout: 20 # seconds action_server_result_timeout: 900.0 default_bt_xml_filename: "navigate_to_pose_bt.xml" plugin_lib_names: - nav2_compute_path_to_pose_action_bt_node - nav2_follow_path_action_bt_node - nav2_back_up_action_bt_node - nav2_spin_action_bt_node - nav2_wait_action_bt_node - nav2_clear_costmap_service_bt_node 3. Costmaps 3.1 Global Costmap # global_costmap params global_costmap: ros__parameters: use_sim_time: true global_frame: map robot_base_frame: base_link update_frequency: 1.0 publish_frequency: 1.0 static_map: true rolling_window: false resolution: 0.05 width: 50.0 height: 50.0 origin_x: -25.0 origin_y: -25.0 plugins: - {name: static_layer, type: "nav2_costmap_2d::StaticLayer"} - {name: inflation_layer, type: "nav2_costmap_2d::InflationLayer"} - {name: voxel_layer, type: "nav2_costmap_2d::VoxelLayer"} - {name: range_layer, type: "nav2_costmap_2d::RangeSensorLayer"} static_layer: plugin: "nav2_costmap_2d::StaticLayer" map_topic: /map subscribe_to_updates: true lethal_cost_threshold: 100 inflation_layer: plugin: "nav2_costmap_2d::InflationLayer" inflation_radius: 0.55 cost_scaling_factor: 3.0 voxel_layer: plugin: "nav2_costmap_2d::VoxelLayer" enabled: true origin_z: -0.1 z_resolution: 0.05 z_voxels: 10 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 obstacle_max_range: 2.5 3.2 Local Costmap # local_costmap params local_costmap: ros__parameters: use_sim_time: true global_frame: odom robot_base_frame: base_link update_frequency: 5.0 publish_frequency: 2.0 static_map: false rolling_window: true width: 5.0 height: 5.0 resolution: 0.05 plugins: - {name: obstacle_layer, type: "nav2_costmap_2d::ObstacleLayer"} - {name: inflation_layer, type: "nav2_costmap_2d::InflationLayer"} - {name: range_layer, type: "nav2_costmap_2d::RangeSensorLayer"} obstacle_layer: plugin: "nav2_costmap_2d::ObstacleLayer" enabled: true observation_sources: "scan" scan: topic: /scan max_obstacle_height: 2.0 clearing: true marking: true data_type: "LaserScan" raytrace_max_range: 3.0 obstacle_max_range: 2.5 min_obstacle_height: 0.0 max_obstacle_height: 2.0 inflation_layer: plugin: "nav2_costmap_2d::InflationLayer" inflation_radius: 0.55 cost_scaling_factor: 3.0 3.3 Costmap Filters (Nav2 Costmap Filters) filter_info: type: 0 # 0=keepout, 1=speed filter_mask_topic: "/costmap_filter_mask" base: 0 multiplier: 1.0 keepout_filter: type: 0 filter_info_topic: "/costmap_filter_info" enabled: true filter_mask_topic: "/keepout_filter_mask" base: 0 multiplier: 1.0 speed_filter: type: 1 # Speed filter filter_info_topic: "/speed_filter_info" enabled: true filter_mask_topic: "/speed_filter_mask" base: 0 multiplier: 0.5 # 50% speed in zone 3.4 Custom Costmap Layer # Custom costmap layer plugin #include #include namespace custom_layers { class DynamicObstacleLayer : public nav2_costmap_2d::Layer { public: void onInitialize() override { ros::NodeHandle nh("~/" + name_); current_ = true; sub_ = nh.subscribe("dynamic_obstacles", 1, &DynamicObstacleLayer::obstacleCallback, this); } void updateBounds(double robot_x, double robot_y, double robot_yaw, double* min_x, double* min_y, double* max_x, double* max_y) override { std::lock_guard lock(mutex_); for (const auto& obs : obstacles_) { *min_x = std::min(*min_x, obs.x - obs.radius); *min_y = std::min(*min_y, obs.y - obs.radius); *max_x = std::max(*max_x, obs.x + obs.radius); *max_y = std::max(*max_y, obs.y + obs.radius); } } void updateCosts(nav2_costmap_2d::Costmap2D& master_grid, int min_i, int min_j, int max_i, int max_j) override { std::lock_guard lock(mutex_); for (const auto& obs : obstacles_) { int mx, my; if (master_grid.worldToMap(obs.x, obs.y, mx, my)) { int radius_cells = obs.radius / master_grid.getResolution(); for (int dx = -radius_cells; dx <= radius_cells; ++dx) { for (int dy = -radius_cells; dy <= radius_cells; ++dy) { if (dx*dx + dy*dy <= radius_cells*radius_cells) { master_grid.setCost(mx+dx, my+dy, nav2_costmap_2d::LETHAL_OBSTACLE); } } } } } } }; } #include PLUGINLIB_EXPORT_CLASS(custom_layers::DynamicObstacleLayer, nav2_costmap_2d::Layer) 4. Global Planners 4.1 Planner Server Configuration # planner_server params planner_server: ros__parameters: use_sim_time: true planner_plugins: ["GridBased", "SmacPlanner"] GridBased: plugin: "nav2_navfn_planner/NavfnPlanner" tolerance: 0.5 use_astar: true allow_unknown: true visualize: false SmacPlanner: plugin: "nav2_smac_planner/SmacPlannerHybrid" tolerance: 0.25 downsample_costmap: true downsampling_factor: 1 allow_unknown: true max_iterations: 1000000 max_on_approach_iterations: 1000 terminal_checking_interval: 10 motion_model_for_search: "DUBIN" analytic_expansion_ratio: 3.5 analytic_expansion_max_length: 3.0 analytic_expansion_max_cost: 1.2 cache_obstacle_heuristic: true allow_reversing: true 4.2 SMAC Planner Variants Planner Motion Model Use Case SmacPlanner2D 2D grid (4/8-connected) Differential drive, omnidirectional SmacPlannerHybrid Hybrid A* (Reeds-Shepp/Dubins) Ackermann, car-like SmacPlannerLattice State lattice High-dimension, custom motions 5. Local Controllers 5.1 Controller Server Configuration # controller_server params controller_server: ros__parameters: use_sim_time: true controller_plugins: ["FollowPath"] FollowPath: plugin: "nav2_dwb_controller/DWBController" debug_trajectory_details: true min_vel_x: 0.0 min_vel_y: 0.0 min_vel_theta: 0.0 max_vel_x: 0.5 max_vel_y: 0.0 max_vel_theta: 1.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 sim_granularity: 0.025 xy_goal_tolerance: 0.25 yaw_goal_tolerance: 0.25 trans_stopped_vel: 0.025 short_circuit_trajectory_evaluation: true critics: - "ObstacleCritic" - "GoalAlignCritic" - "GoalDistCritic" - "PathAlignCritic" - "PathDistCritic" - "TwirlingCritic" - "VelocityDeadbandCritic" ObstacleCritic: cost_power: 1.0 repulsion_weight: 1.5 critical_cost: 254 consider_footprint: true GoalAlignCritic: scale: 1.0 cost_power: 1.0 5.2 Alternative Controllers TEB (Timed Elastic Band) TEBController: plugin: "nav2_teb_controller/TebController" trajectory: teb_autosize: true dt_ref: 0.3 dt_hysteresis: 0.1 global_plan_overwrite_orientation: true max_global_plan_lookahead_dist: 3.0 force_reinit_new_goal_dist: 1.0 force_reinit_new_goal_angular: 0.5 robot: max_vel_x: 0.5 max_vel_x_backwards: 0.2 max_vel_y: 0.0 max_vel_theta: 1.0 acc_lim_x: 2.5 acc_lim_theta: 3.2 min_turning_radius: 0.0 # 0 for diff drive footprint_model: type: "polygon" vertices: [[0.3, 0.2], [0.3, -0.2], [-0.3, -0.2], [-0.3, 0.2]] goal_tolerance: xy_goal_tolerance: 0.2 yaw_goal_tolerance: 0.1 obstacles: min_obstacle_dist: 0.5 inflation_dist: 1.0 include_costmap_obstacles: true costmap_obstacles_behind_robot_dist: 1.5 optim: weight_max_vel_x: 2.0 weight_max_vel_theta: 1.0 weight_acc_lim_x: 1.0 weight_acc_lim_theta: 1.0 weight_kinematics_nh: 1000.0 weight_kinematics_fwd_drive: 10.0 weight_obstacle: 50.0 weight_dynamic_obstacle: 10.0 weight_viapoint: 1.0 weight_adapt_factor: 2.0 MPPI (Model Predictive Path Integral) MPPIController: plugin: "nav2_mppi_controller/MPPIController" time_steps: 56 model_dt: 0.05 batch_size: 2000 vx_std: 0.2 vy_std: 0.0 vtheta_std: 0.4 vx_max: 0.5 vtheta_max: 1.0 iteration_count: 1 prune_distance: 1.5 transform_tolerance: 0.1 temperature: 0.3 gamma: 0.015 costs: - {name: "CriticCost", weight: 1.0, cost_power: 1.0} - {name: "GoalCost", weight: 1.0, cost_power: 1.0} - {name: "ObstacleCost", weight: 1.0, cost_power: 1.0} 6. Recovery Behaviors 6.1 Built-in Recoveries # behavior_server params behavior_server: ros__parameters: use_sim_time: true local_costmap_topic: local_costmap/costmap_raw global_costmap_topic: global_costmap/costmap_raw local_footprint_topic: local_costmap/published_footprint global_footprint_topic: global_costmap/published_footprint cycle_frequency: 10.0 behavior_plugins: ["spin", "backup", "wait", "clear_costmap"] spin: plugin: "nav2_behaviors/Spin" spin_dist: 1.57 spin_speed: 0.5 simulate_ahead_time: 2.0 max_rotational_vel: 1.0 min_rotational_vel: 0.4 rotational_acc_lim: 3.2 backup: plugin: "nav2_behaviors/BackUp" backup_dist: 0.15 backup_speed: 0.025 time_allowance: 10.0 wait: plugin: "nav2_behaviors/Wait" wait_duration: 5.0 clear_costmap: plugin: "nav2_behaviors/ClearCostmap" service_name: "global_costmap/clear_entirely_global_costmap" 6.2 Custom Recovery Behavior # Custom recovery (C++) #include class DockRecovery : public nav2_behaviors::BehaviorBase { public: DockRecovery() : nav2_behaviors::BehaviorBase("dock_recovery") {} void onConfigure() override { dock_client_ = action_client (node_, "dock_robot"); } void onActivate() override { auto goal = DockAction::Goal(); goal.dock_id = "home"; auto future = dock_client_->async_send_goal(goal); // Handle result... } void onDeactivate() override { dock_client_->async_cancel_all_goals(); } nav2_behaviors::BehaviorStatus onRun(const std::shared_ptr & context) override { // Check if docking succeeded if (dock_succeeded_) return Status::SUCCEEDED; if (dock_failed_) return Status::FAILED; return Status::RUNNING; } }; 7. Advanced: Multi-Robot Navigation 7.1 Namespaced Nav2 7.2 Collision Avoidance Between Robots # Costmap layer for other robots # Other robots appear as dynamic obstacles # Custom layer subscribes to /robot2/pose, /robot3/pose # Adds them as circular obstacles to local costmap # Or use Nav2's Multi-Robot collision checker (experimental) collision_checker: plugin: "nav2_collision_checker/CollisionChecker" robots: - {name: "robot2", radius: 0.4, topic: "/robot2/pose"} - {name: "robot3", radius: 0.4, topic: "/robot3/pose"} 8. Nav2 with Custom Messages 8.1 Custom Goal Checker # Goal checker plugin #include class PrecisionGoalChecker : public nav2_core::GoalChecker { public: void initialize(const rclcpp_lifecycle::LifecycleNode::SharedPtr parent, const std::string& plugin_name) override { node_ = parent; plugin_name_ = plugin_name; xy_tolerance_ = 0.05; // 5cm yaw_tolerance_ = 0.05; // ~3 deg } bool isGoalReached(const geometry_msgs::msg::Pose& query_pose, const geometry_msgs::msg::Pose& goal_pose, const nav2_msgs::msg::GoalInfo& goal_info) override { double dx = query_pose.position.x - goal_pose.position.x; double dy = query_pose.position.y - goal_pose.position.y double dist = std::hypot(dx, dy); double dyaw = angles::shortest_angular_distance( tf2::getYaw(query_pose.orientation), tf2::getYaw(goal_pose.orientation)); return (dist <= xy_tolerance_) && (std::abs(dya

Comments

Popular posts from this blog

Firmware Testing Strategies: Hardware-in-the-Loop, Simulation, and CI/CD Pipelines | Embedded Systems Hub

IoT Protocols Compared: MQTT, CoAP, HTTP/2, WebSocket - Performance, Overhead, and Use Cases | Embedded Systems Hub

Embedded Security: Secure Boot, ARM TrustZone, and OTA Update Integrity | Embedded Systems Hub