给轨迹+MPPI版导航加入了障碍物检测
This commit is contained in:
@@ -112,8 +112,8 @@ controller_server:
|
|||||||
CostCritic:
|
CostCritic:
|
||||||
enabled: true
|
enabled: true
|
||||||
cost_power: 1
|
cost_power: 1
|
||||||
cost_weight: 4.5
|
cost_weight: 6.0
|
||||||
critical_cost: 300.0
|
critical_cost: 250.0
|
||||||
consider_footprint: true
|
consider_footprint: true
|
||||||
collision_cost: 1000000.0
|
collision_cost: 1000000.0
|
||||||
near_goal_distance: 0.8
|
near_goal_distance: 0.8
|
||||||
@@ -134,7 +134,7 @@ controller_server:
|
|||||||
PathAlignCritic:
|
PathAlignCritic:
|
||||||
enabled: true
|
enabled: true
|
||||||
cost_power: 1
|
cost_power: 1
|
||||||
cost_weight: 16.0
|
cost_weight: 8.0
|
||||||
max_path_occupancy_ratio: 0.07
|
max_path_occupancy_ratio: 0.07
|
||||||
trajectory_point_step: 4
|
trajectory_point_step: 4
|
||||||
threshold_to_consider: 0.45
|
threshold_to_consider: 0.45
|
||||||
@@ -144,14 +144,14 @@ controller_server:
|
|||||||
PathFollowCritic:
|
PathFollowCritic:
|
||||||
enabled: true
|
enabled: true
|
||||||
cost_power: 1
|
cost_power: 1
|
||||||
cost_weight: 6.0
|
cost_weight: 4.0
|
||||||
offset_from_furthest: 4
|
offset_from_furthest: 4
|
||||||
threshold_to_consider: 1.0
|
threshold_to_consider: 1.0
|
||||||
|
|
||||||
PathAngleCritic:
|
PathAngleCritic:
|
||||||
enabled: true
|
enabled: true
|
||||||
cost_power: 1
|
cost_power: 1
|
||||||
cost_weight: 6.0
|
cost_weight: 5.0
|
||||||
offset_from_furthest: 5
|
offset_from_furthest: 5
|
||||||
threshold_to_consider: 0.45
|
threshold_to_consider: 0.45
|
||||||
max_angle_to_furthest: 1.2
|
max_angle_to_furthest: 1.2
|
||||||
@@ -160,7 +160,7 @@ controller_server:
|
|||||||
PreferForwardCritic:
|
PreferForwardCritic:
|
||||||
enabled: true
|
enabled: true
|
||||||
cost_power: 1
|
cost_power: 1
|
||||||
cost_weight: 2.0
|
cost_weight: 1.0
|
||||||
threshold_to_consider: 0.4
|
threshold_to_consider: 0.4
|
||||||
|
|
||||||
controller_server_rclcpp_node:
|
controller_server_rclcpp_node:
|
||||||
@@ -188,12 +188,13 @@ local_costmap:
|
|||||||
plugin: "obstacle_nav2::ObstacleArrayLayer"
|
plugin: "obstacle_nav2::ObstacleArrayLayer"
|
||||||
enabled: true
|
enabled: true
|
||||||
topic: /obstacles
|
topic: /obstacles
|
||||||
obstacle_timeout: 0.5
|
obstacle_timeout: 1.0
|
||||||
transform_tolerance: 0.2
|
transform_tolerance: 0.2
|
||||||
default_obstacle_radius: 0.05
|
default_obstacle_radius: 0.05
|
||||||
minimum_obstacle_radius: 0.02
|
minimum_obstacle_radius: 0.02
|
||||||
maximum_obstacle_radius: 0.06
|
maximum_obstacle_radius: 0.06
|
||||||
extra_inflation: 0.02
|
extra_inflation: 0.02
|
||||||
|
retain_previous_on_empty_snapshot: true
|
||||||
inflation_layer:
|
inflation_layer:
|
||||||
plugin: "nav2_costmap_2d::InflationLayer"
|
plugin: "nav2_costmap_2d::InflationLayer"
|
||||||
cost_scaling_factor: 3.0
|
cost_scaling_factor: 3.0
|
||||||
@@ -209,8 +210,8 @@ local_costmap:
|
|||||||
global_costmap:
|
global_costmap:
|
||||||
global_costmap:
|
global_costmap:
|
||||||
ros__parameters:
|
ros__parameters:
|
||||||
update_frequency: 2.0
|
update_frequency: 5.0
|
||||||
publish_frequency: 1.0
|
publish_frequency: 3.0
|
||||||
transform_tolerance: 0.5
|
transform_tolerance: 0.5
|
||||||
global_frame: odom
|
global_frame: odom
|
||||||
robot_base_frame: base_footprint
|
robot_base_frame: base_footprint
|
||||||
@@ -227,12 +228,13 @@ global_costmap:
|
|||||||
plugin: "obstacle_nav2::ObstacleArrayLayer"
|
plugin: "obstacle_nav2::ObstacleArrayLayer"
|
||||||
enabled: true
|
enabled: true
|
||||||
topic: /obstacles
|
topic: /obstacles
|
||||||
obstacle_timeout: 0.5
|
obstacle_timeout: 1.0
|
||||||
transform_tolerance: 0.2
|
transform_tolerance: 0.2
|
||||||
default_obstacle_radius: 0.05
|
default_obstacle_radius: 0.05
|
||||||
minimum_obstacle_radius: 0.02
|
minimum_obstacle_radius: 0.02
|
||||||
maximum_obstacle_radius: 0.50
|
maximum_obstacle_radius: 0.50
|
||||||
extra_inflation: 0.02
|
extra_inflation: 0.02
|
||||||
|
retain_previous_on_empty_snapshot: true
|
||||||
inflation_layer:
|
inflation_layer:
|
||||||
plugin: "nav2_costmap_2d::InflationLayer"
|
plugin: "nav2_costmap_2d::InflationLayer"
|
||||||
cost_scaling_factor: 3.0
|
cost_scaling_factor: 3.0
|
||||||
@@ -264,10 +266,10 @@ planner_server:
|
|||||||
analytic_expansion_max_length: 2.0
|
analytic_expansion_max_length: 2.0
|
||||||
minimum_turning_radius: 0.60
|
minimum_turning_radius: 0.60
|
||||||
reverse_penalty: 2.2
|
reverse_penalty: 2.2
|
||||||
change_penalty: 0.5
|
change_penalty: 0.0
|
||||||
non_straight_penalty: 0.85
|
non_straight_penalty: 0.8
|
||||||
cost_penalty: 4.5
|
cost_penalty: 5.0
|
||||||
retrospective_penalty: 0.015
|
retrospective_penalty: 0.01
|
||||||
lookup_table_size: 5.0
|
lookup_table_size: 5.0
|
||||||
cache_obstacle_heuristic: false
|
cache_obstacle_heuristic: false
|
||||||
viz_expansions: false
|
viz_expansions: false
|
||||||
|
|||||||
334
src/navigation/obstacle_nav2/config/nav2_profile_10.yaml
Normal file
334
src/navigation/obstacle_nav2/config/nav2_profile_10.yaml
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# nav2_params.yaml — Odometry-only obstacle navigation
|
||||||
|
#
|
||||||
|
# No static map, no AMCL, no SLAM.
|
||||||
|
# Both costmaps are rolling windows in odom. The launch file can rewrite every
|
||||||
|
# global_frame leaf when a different connected odometry frame is required.
|
||||||
|
# Global planner: Smac Hybrid A* (Reeds-Shepp)
|
||||||
|
# Local controller: MPPI (Ackermann)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
bt_navigator:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
global_frame: odom
|
||||||
|
robot_base_frame: base_footprint
|
||||||
|
odom_topic: /odom_combined
|
||||||
|
bt_loop_duration: 50
|
||||||
|
default_server_timeout: 20
|
||||||
|
# Injected by obstacle_nav2.launch.py from this package's share directory.
|
||||||
|
default_nav_to_pose_bt_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_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
|
||||||
|
|
||||||
|
bt_navigator_rclcpp_node:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
|
||||||
|
controller_server:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
controller_frequency: 20.0
|
||||||
|
FollowPath:
|
||||||
|
plugin: "nav2_mppi_controller::MPPIController"
|
||||||
|
time_steps: 36
|
||||||
|
model_dt: 0.05
|
||||||
|
batch_size: 1000
|
||||||
|
vx_std: 0.75
|
||||||
|
vy_std: 0.0
|
||||||
|
wz_std: 0.4
|
||||||
|
vx_max: 1.75
|
||||||
|
vx_min: -0.75
|
||||||
|
vy_max: 0.0
|
||||||
|
wz_max: 1.5
|
||||||
|
iteration_count: 1
|
||||||
|
temperature: 0.3
|
||||||
|
gamma: 0.015
|
||||||
|
motion_model: "Ackermann"
|
||||||
|
visualize: false
|
||||||
|
TrajectoryVisualizer:
|
||||||
|
trajectory_step: 5
|
||||||
|
time_step: 3
|
||||||
|
AckermannConstraints:
|
||||||
|
min_turning_r: 0.4
|
||||||
|
critics: ["ConstraintCritic", "CostCritic", "GoalCritic", "GoalAngleCritic", "PathAlignCritic", "PathFollowCritic", "PathAngleCritic", "PreferForwardCritic"]
|
||||||
|
ConstraintCritic:
|
||||||
|
enabled: true
|
||||||
|
cost_power: 1
|
||||||
|
cost_weight: 4.0
|
||||||
|
GoalCritic:
|
||||||
|
enabled: true
|
||||||
|
cost_power: 1
|
||||||
|
cost_weight: 5.0
|
||||||
|
threshold_to_consider: 1.4
|
||||||
|
GoalAngleCritic:
|
||||||
|
enabled: true
|
||||||
|
cost_power: 1
|
||||||
|
cost_weight: 3.0
|
||||||
|
threshold_to_consider: 0.5
|
||||||
|
PreferForwardCritic:
|
||||||
|
enabled: false
|
||||||
|
cost_power: 1
|
||||||
|
cost_weight: 0.0
|
||||||
|
threshold_to_consider: 0.5
|
||||||
|
CostCritic:
|
||||||
|
enabled: true
|
||||||
|
cost_power: 1
|
||||||
|
cost_weight: 3.81
|
||||||
|
critical_cost: 300.0
|
||||||
|
consider_footprint: true
|
||||||
|
collision_cost: 1000000.0
|
||||||
|
near_goal_distance: 1.0
|
||||||
|
trajectory_point_step: 2
|
||||||
|
PathAlignCritic:
|
||||||
|
enabled: true
|
||||||
|
cost_power: 1
|
||||||
|
cost_weight: 14.0
|
||||||
|
max_path_occupancy_ratio: 0.05
|
||||||
|
trajectory_point_step: 4
|
||||||
|
threshold_to_consider: 0.5
|
||||||
|
offset_from_furthest: 20
|
||||||
|
use_path_orientations: false
|
||||||
|
PathFollowCritic:
|
||||||
|
enabled: true
|
||||||
|
cost_power: 1
|
||||||
|
cost_weight: 5.0
|
||||||
|
offset_from_furthest: 5
|
||||||
|
threshold_to_consider: 1.4
|
||||||
|
PathAngleCritic:
|
||||||
|
enabled: true
|
||||||
|
cost_power: 1
|
||||||
|
cost_weight: 2.0
|
||||||
|
offset_from_furthest: 4
|
||||||
|
threshold_to_consider: 0.5
|
||||||
|
max_angle_to_furthest: 1.0
|
||||||
|
forward_preference: false
|
||||||
|
|
||||||
|
controller_server_rclcpp_node:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
|
||||||
|
local_costmap:
|
||||||
|
local_costmap:
|
||||||
|
ros__parameters:
|
||||||
|
update_frequency: 5.0
|
||||||
|
publish_frequency: 2.0
|
||||||
|
transform_tolerance: 0.5
|
||||||
|
global_frame: odom
|
||||||
|
robot_base_frame: base_footprint
|
||||||
|
use_sim_time: False
|
||||||
|
rolling_window: true
|
||||||
|
width: 3
|
||||||
|
height: 3
|
||||||
|
resolution: 0.05
|
||||||
|
footprint: "[[0.14, 0.085], [0.14, -0.085], [-0.14, -0.085], [-0.14, 0.085]]"
|
||||||
|
footprint_padding: 0.02
|
||||||
|
track_unknown_space: false
|
||||||
|
plugins: ["obstacle_array_layer", "inflation_layer"]
|
||||||
|
obstacle_array_layer:
|
||||||
|
plugin: "obstacle_nav2::ObstacleArrayLayer"
|
||||||
|
enabled: true
|
||||||
|
topic: /obstacles
|
||||||
|
obstacle_timeout: 0.5
|
||||||
|
transform_tolerance: 0.2
|
||||||
|
default_obstacle_radius: 0.05
|
||||||
|
minimum_obstacle_radius: 0.02
|
||||||
|
maximum_obstacle_radius: 0.50
|
||||||
|
extra_inflation: 0.02
|
||||||
|
inflation_layer:
|
||||||
|
plugin: "nav2_costmap_2d::InflationLayer"
|
||||||
|
cost_scaling_factor: 3.0
|
||||||
|
inflation_radius: 0.55
|
||||||
|
always_send_full_costmap: True
|
||||||
|
local_costmap_client:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
local_costmap_rclcpp_node:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
|
||||||
|
global_costmap:
|
||||||
|
global_costmap:
|
||||||
|
ros__parameters:
|
||||||
|
update_frequency: 1.0
|
||||||
|
publish_frequency: 1.0
|
||||||
|
transform_tolerance: 0.5
|
||||||
|
global_frame: odom
|
||||||
|
robot_base_frame: base_footprint
|
||||||
|
use_sim_time: False
|
||||||
|
rolling_window: true
|
||||||
|
width: 10
|
||||||
|
height: 10
|
||||||
|
resolution: 0.05
|
||||||
|
footprint: "[[0.14, 0.085], [0.14, -0.085], [-0.14, -0.085], [-0.14, 0.085]]"
|
||||||
|
footprint_padding: 0.02
|
||||||
|
track_unknown_space: false
|
||||||
|
plugins: ["obstacle_array_layer", "inflation_layer"]
|
||||||
|
obstacle_array_layer:
|
||||||
|
plugin: "obstacle_nav2::ObstacleArrayLayer"
|
||||||
|
enabled: true
|
||||||
|
topic: /obstacles
|
||||||
|
obstacle_timeout: 0.5
|
||||||
|
transform_tolerance: 0.2
|
||||||
|
default_obstacle_radius: 0.05
|
||||||
|
minimum_obstacle_radius: 0.02
|
||||||
|
maximum_obstacle_radius: 0.50
|
||||||
|
extra_inflation: 0.02
|
||||||
|
inflation_layer:
|
||||||
|
plugin: "nav2_costmap_2d::InflationLayer"
|
||||||
|
cost_scaling_factor: 3.0
|
||||||
|
inflation_radius: 0.55
|
||||||
|
always_send_full_costmap: True
|
||||||
|
global_costmap_client:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
global_costmap_rclcpp_node:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
|
||||||
|
planner_server:
|
||||||
|
ros__parameters:
|
||||||
|
planner_plugins: ["GridBased"]
|
||||||
|
use_sim_time: False
|
||||||
|
GridBased:
|
||||||
|
plugin: "nav2_smac_planner/SmacPlannerHybrid"
|
||||||
|
downsample_costmap: false
|
||||||
|
downsampling_factor: 1
|
||||||
|
tolerance: 0.25
|
||||||
|
allow_unknown: false
|
||||||
|
max_iterations: 1000000
|
||||||
|
max_on_approach_iterations: 1000
|
||||||
|
max_planning_time: 5.0
|
||||||
|
motion_model_for_search: "REEDS_SHEPP"
|
||||||
|
angle_quantization_bins: 72
|
||||||
|
analytic_expansion_ratio: 3.5
|
||||||
|
analytic_expansion_max_length: 3.0
|
||||||
|
minimum_turning_radius: 0.40
|
||||||
|
reverse_penalty: 1.0
|
||||||
|
change_penalty: 0.0
|
||||||
|
non_straight_penalty: 1.2
|
||||||
|
cost_penalty: 2.0
|
||||||
|
retrospective_penalty: 0.015
|
||||||
|
# 5 m covers the rolling planning horizon without the startup and memory
|
||||||
|
# cost of the previous 20 m (401-cell) Hybrid-A* lookup table.
|
||||||
|
lookup_table_size: 5.0
|
||||||
|
cache_obstacle_heuristic: false
|
||||||
|
viz_expansions: false
|
||||||
|
smooth_path: True
|
||||||
|
smoother:
|
||||||
|
max_iterations: 1000
|
||||||
|
w_smooth: 0.3
|
||||||
|
w_data: 0.2
|
||||||
|
tolerance: 1.0e-10
|
||||||
|
do_refinement: true
|
||||||
|
refinement_num: 2
|
||||||
|
|
||||||
|
planner_server_rclcpp_node:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
|
||||||
|
smoother_server:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
smoother_plugins: ["simple_smoother"]
|
||||||
|
simple_smoother:
|
||||||
|
plugin: "nav2_smoother::SimpleSmoother"
|
||||||
|
tolerance: 1.0e-10
|
||||||
|
max_its: 1000
|
||||||
|
do_refinement: True
|
||||||
|
|
||||||
|
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"
|
||||||
|
backup_dist: 0.8
|
||||||
|
backup_speed: 0.18
|
||||||
|
wait:
|
||||||
|
plugin: "nav2_behaviors/Wait"
|
||||||
|
wait_duration: 0.5
|
||||||
|
global_frame: odom
|
||||||
|
robot_base_frame: base_footprint
|
||||||
|
transform_tolerance: 0.5
|
||||||
|
use_sim_time: False
|
||||||
|
simulate_ahead_time: 2.0
|
||||||
|
max_rotational_vel: 1.0
|
||||||
|
min_rotational_vel: 0.4
|
||||||
|
rotational_acc_lim: 3.2
|
||||||
|
|
||||||
|
robot_state_publisher:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
|
||||||
|
waypoint_follower:
|
||||||
|
ros__parameters:
|
||||||
|
loop_rate: 20
|
||||||
|
use_sim_time: False
|
||||||
|
stop_on_failure: false
|
||||||
|
waypoint_task_executor_plugin: "wait_at_waypoint"
|
||||||
|
wait_at_waypoint:
|
||||||
|
plugin: "nav2_waypoint_follower::WaitAtWaypoint"
|
||||||
|
enabled: True
|
||||||
|
waypoint_pause_duration: 200
|
||||||
|
|
||||||
|
velocity_smoother:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
smoothing_frequency: 20.0
|
||||||
|
scale_velocities: False
|
||||||
|
feedback: "OPEN_LOOP"
|
||||||
|
max_velocity: [1.75, 0.0, 1.5]
|
||||||
|
min_velocity: [-0.75, 0.0, -1.5]
|
||||||
|
max_accel: [2.5, 0.0, 3.2]
|
||||||
|
max_decel: [-2.5, 0.0, -3.2]
|
||||||
|
odom_topic: /odom_combined
|
||||||
|
odom_duration: 0.1
|
||||||
|
deadband_velocity: [0.03, 0.0, 0.03]
|
||||||
|
velocity_timeout: 1.0
|
||||||
358
src/navigation/obstacle_nav2/config/nav2_profile_11.yaml
Normal file
358
src/navigation/obstacle_nav2/config/nav2_profile_11.yaml
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# nav2_params_basic_tracking.yaml
|
||||||
|
#
|
||||||
|
# Basic stable trajectory-following profile for obstacle_nav2.
|
||||||
|
# This file is intentionally conservative: low speed, longer MPPI horizon,
|
||||||
|
# smoother acceleration, moderate path tracking weights, and Ackermann limits.
|
||||||
|
#
|
||||||
|
# It does not replace nav2_params.yaml. To use it, launch Nav2 with this params
|
||||||
|
# file or add a launch argument later.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
bt_navigator:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
global_frame: odom
|
||||||
|
robot_base_frame: base_footprint
|
||||||
|
odom_topic: /odom_combined
|
||||||
|
bt_loop_duration: 50
|
||||||
|
default_server_timeout: 20
|
||||||
|
default_nav_to_pose_bt_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_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
|
||||||
|
|
||||||
|
bt_navigator_rclcpp_node:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
|
||||||
|
controller_server:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
controller_frequency: 20.0
|
||||||
|
|
||||||
|
FollowPath:
|
||||||
|
plugin: "nav2_mppi_controller::MPPIController"
|
||||||
|
|
||||||
|
# Longer horizon and moderate sampling make turns easier to discover.
|
||||||
|
time_steps: 48
|
||||||
|
model_dt: 0.05
|
||||||
|
batch_size: 1000
|
||||||
|
iteration_count: 1
|
||||||
|
|
||||||
|
# Conservative velocity exploration. Speed is deliberately not optimized.
|
||||||
|
vx_std: 0.20
|
||||||
|
vy_std: 0.0
|
||||||
|
wz_std: 0.85
|
||||||
|
vx_max: 0.50
|
||||||
|
vx_min: -0.15
|
||||||
|
vy_max: 0.0
|
||||||
|
wz_max: 1.8
|
||||||
|
|
||||||
|
# Lower temperature makes selection steadier; gamma damps rough controls.
|
||||||
|
temperature: 0.25
|
||||||
|
gamma: 0.02
|
||||||
|
|
||||||
|
motion_model: "Ackermann"
|
||||||
|
visualize: false
|
||||||
|
TrajectoryVisualizer:
|
||||||
|
trajectory_step: 5
|
||||||
|
time_step: 3
|
||||||
|
AckermannConstraints:
|
||||||
|
min_turning_r: 0.60
|
||||||
|
|
||||||
|
critics: ["ConstraintCritic", "CostCritic", "GoalCritic", "GoalAngleCritic", "PathAlignCritic", "PathFollowCritic", "PathAngleCritic", "PreferForwardCritic"]
|
||||||
|
|
||||||
|
ConstraintCritic:
|
||||||
|
enabled: true
|
||||||
|
cost_power: 1
|
||||||
|
cost_weight: 3.0
|
||||||
|
|
||||||
|
CostCritic:
|
||||||
|
enabled: true
|
||||||
|
cost_power: 1
|
||||||
|
cost_weight: 6.0
|
||||||
|
critical_cost: 250.0
|
||||||
|
consider_footprint: true
|
||||||
|
collision_cost: 1000000.0
|
||||||
|
near_goal_distance: 0.8
|
||||||
|
trajectory_point_step: 2
|
||||||
|
|
||||||
|
GoalCritic:
|
||||||
|
enabled: true
|
||||||
|
cost_power: 1
|
||||||
|
cost_weight: 2.0
|
||||||
|
threshold_to_consider: 1.0
|
||||||
|
|
||||||
|
GoalAngleCritic:
|
||||||
|
enabled: true
|
||||||
|
cost_power: 1
|
||||||
|
cost_weight: 2.0
|
||||||
|
threshold_to_consider: 0.45
|
||||||
|
|
||||||
|
PathAlignCritic:
|
||||||
|
enabled: true
|
||||||
|
cost_power: 1
|
||||||
|
cost_weight: 8.0
|
||||||
|
max_path_occupancy_ratio: 0.07
|
||||||
|
trajectory_point_step: 4
|
||||||
|
threshold_to_consider: 0.45
|
||||||
|
offset_from_furthest: 8
|
||||||
|
use_path_orientations: false
|
||||||
|
|
||||||
|
PathFollowCritic:
|
||||||
|
enabled: true
|
||||||
|
cost_power: 1
|
||||||
|
cost_weight: 4.0
|
||||||
|
offset_from_furthest: 4
|
||||||
|
threshold_to_consider: 1.0
|
||||||
|
|
||||||
|
PathAngleCritic:
|
||||||
|
enabled: true
|
||||||
|
cost_power: 1
|
||||||
|
cost_weight: 5.0
|
||||||
|
offset_from_furthest: 5
|
||||||
|
threshold_to_consider: 0.45
|
||||||
|
max_angle_to_furthest: 1.2
|
||||||
|
forward_preference: false
|
||||||
|
|
||||||
|
PreferForwardCritic:
|
||||||
|
enabled: true
|
||||||
|
cost_power: 1
|
||||||
|
cost_weight: 1.0
|
||||||
|
threshold_to_consider: 0.4
|
||||||
|
|
||||||
|
controller_server_rclcpp_node:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
|
||||||
|
local_costmap:
|
||||||
|
local_costmap:
|
||||||
|
ros__parameters:
|
||||||
|
update_frequency: 8.0
|
||||||
|
publish_frequency: 4.0
|
||||||
|
transform_tolerance: 0.5
|
||||||
|
global_frame: odom
|
||||||
|
robot_base_frame: base_footprint
|
||||||
|
use_sim_time: False
|
||||||
|
rolling_window: true
|
||||||
|
width: 4
|
||||||
|
height: 4
|
||||||
|
resolution: 0.05
|
||||||
|
footprint: "[[0.14, 0.085], [0.14, -0.085], [-0.14, -0.085], [-0.14, 0.085]]"
|
||||||
|
footprint_padding: 0.02
|
||||||
|
track_unknown_space: false
|
||||||
|
plugins: ["obstacle_array_layer", "inflation_layer"]
|
||||||
|
obstacle_array_layer:
|
||||||
|
plugin: "obstacle_nav2::ObstacleArrayLayer"
|
||||||
|
enabled: true
|
||||||
|
topic: /obstacles
|
||||||
|
obstacle_timeout: 1.0
|
||||||
|
transform_tolerance: 0.2
|
||||||
|
default_obstacle_radius: 0.05
|
||||||
|
minimum_obstacle_radius: 0.02
|
||||||
|
maximum_obstacle_radius: 0.06
|
||||||
|
extra_inflation: 0.02
|
||||||
|
retain_previous_on_empty_snapshot: true
|
||||||
|
inflation_layer:
|
||||||
|
plugin: "nav2_costmap_2d::InflationLayer"
|
||||||
|
cost_scaling_factor: 3.0
|
||||||
|
inflation_radius: 0.25
|
||||||
|
always_send_full_costmap: True
|
||||||
|
local_costmap_client:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
local_costmap_rclcpp_node:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
|
||||||
|
global_costmap:
|
||||||
|
global_costmap:
|
||||||
|
ros__parameters:
|
||||||
|
update_frequency: 5.0
|
||||||
|
publish_frequency: 3.0
|
||||||
|
transform_tolerance: 0.5
|
||||||
|
global_frame: odom
|
||||||
|
robot_base_frame: base_footprint
|
||||||
|
use_sim_time: False
|
||||||
|
rolling_window: true
|
||||||
|
width: 10
|
||||||
|
height: 10
|
||||||
|
resolution: 0.05
|
||||||
|
footprint: "[[0.14, 0.085], [0.14, -0.085], [-0.14, -0.085], [-0.14, 0.085]]"
|
||||||
|
footprint_padding: 0.02
|
||||||
|
track_unknown_space: false
|
||||||
|
plugins: ["obstacle_array_layer", "inflation_layer"]
|
||||||
|
obstacle_array_layer:
|
||||||
|
plugin: "obstacle_nav2::ObstacleArrayLayer"
|
||||||
|
enabled: true
|
||||||
|
topic: /obstacles
|
||||||
|
obstacle_timeout: 1.0
|
||||||
|
transform_tolerance: 0.2
|
||||||
|
default_obstacle_radius: 0.05
|
||||||
|
minimum_obstacle_radius: 0.02
|
||||||
|
maximum_obstacle_radius: 0.50
|
||||||
|
extra_inflation: 0.02
|
||||||
|
retain_previous_on_empty_snapshot: true
|
||||||
|
inflation_layer:
|
||||||
|
plugin: "nav2_costmap_2d::InflationLayer"
|
||||||
|
cost_scaling_factor: 3.0
|
||||||
|
inflation_radius: 0.45
|
||||||
|
always_send_full_costmap: True
|
||||||
|
global_costmap_client:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
global_costmap_rclcpp_node:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
|
||||||
|
planner_server:
|
||||||
|
ros__parameters:
|
||||||
|
planner_plugins: ["GridBased"]
|
||||||
|
use_sim_time: False
|
||||||
|
GridBased:
|
||||||
|
plugin: "nav2_smac_planner/SmacPlannerHybrid"
|
||||||
|
downsample_costmap: false
|
||||||
|
downsampling_factor: 1
|
||||||
|
tolerance: 0.20
|
||||||
|
allow_unknown: false
|
||||||
|
max_iterations: 1000000
|
||||||
|
max_on_approach_iterations: 1000
|
||||||
|
max_planning_time: 5.0
|
||||||
|
motion_model_for_search: "REEDS_SHEPP"
|
||||||
|
angle_quantization_bins: 72
|
||||||
|
analytic_expansion_ratio: 3.5
|
||||||
|
analytic_expansion_max_length: 2.0
|
||||||
|
minimum_turning_radius: 0.60
|
||||||
|
reverse_penalty: 2.2
|
||||||
|
change_penalty: 0.0
|
||||||
|
non_straight_penalty: 0.8
|
||||||
|
cost_penalty: 5.0
|
||||||
|
retrospective_penalty: 0.01
|
||||||
|
lookup_table_size: 5.0
|
||||||
|
cache_obstacle_heuristic: false
|
||||||
|
viz_expansions: false
|
||||||
|
smooth_path: True
|
||||||
|
smoother:
|
||||||
|
max_iterations: 1000
|
||||||
|
w_smooth: 0.35
|
||||||
|
w_data: 0.25
|
||||||
|
tolerance: 1.0e-10
|
||||||
|
do_refinement: true
|
||||||
|
refinement_num: 2
|
||||||
|
|
||||||
|
planner_server_rclcpp_node:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
|
||||||
|
smoother_server:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
smoother_plugins: ["simple_smoother"]
|
||||||
|
simple_smoother:
|
||||||
|
plugin: "nav2_smoother::SimpleSmoother"
|
||||||
|
tolerance: 1.0e-10
|
||||||
|
max_its: 1000
|
||||||
|
do_refinement: True
|
||||||
|
|
||||||
|
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"
|
||||||
|
backup_dist: 0.30
|
||||||
|
backup_speed: 0.08
|
||||||
|
wait:
|
||||||
|
plugin: "nav2_behaviors/Wait"
|
||||||
|
wait_duration: 0.5
|
||||||
|
global_frame: odom
|
||||||
|
robot_base_frame: base_footprint
|
||||||
|
transform_tolerance: 0.5
|
||||||
|
use_sim_time: False
|
||||||
|
simulate_ahead_time: 2.0
|
||||||
|
max_rotational_vel: 0.8
|
||||||
|
min_rotational_vel: 0.2
|
||||||
|
rotational_acc_lim: 1.5
|
||||||
|
|
||||||
|
robot_state_publisher:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
|
||||||
|
waypoint_follower:
|
||||||
|
ros__parameters:
|
||||||
|
loop_rate: 20
|
||||||
|
use_sim_time: False
|
||||||
|
stop_on_failure: false
|
||||||
|
waypoint_task_executor_plugin: "wait_at_waypoint"
|
||||||
|
wait_at_waypoint:
|
||||||
|
plugin: "nav2_waypoint_follower::WaitAtWaypoint"
|
||||||
|
enabled: True
|
||||||
|
waypoint_pause_duration: 200
|
||||||
|
|
||||||
|
velocity_smoother:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
smoothing_frequency: 20.0
|
||||||
|
scale_velocities: False
|
||||||
|
feedback: "OPEN_LOOP"
|
||||||
|
max_velocity: [0.50, 0.0, 1.8]
|
||||||
|
min_velocity: [-0.15, 0.0, -1.8]
|
||||||
|
max_accel: [0.50, 0.0, 1.50]
|
||||||
|
max_decel: [-0.60, 0.0, -1.80]
|
||||||
|
odom_topic: /odom_combined
|
||||||
|
odom_duration: 0.1
|
||||||
|
deadband_velocity: [0.02, 0.0, 0.02]
|
||||||
|
velocity_timeout: 1.0
|
||||||
|
|
||||||
|
lifecycle_manager_navigation:
|
||||||
|
ros__parameters:
|
||||||
|
use_sim_time: False
|
||||||
|
autostart: True
|
||||||
|
service_timeout: 10.0
|
||||||
|
bond_timeout: 8.0
|
||||||
27
src/navigation/obstacle_nav2/config/trajectory_guard.yaml
Normal file
27
src/navigation/obstacle_nav2/config/trajectory_guard.yaml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
trajectory_guard_node:
|
||||||
|
ros__parameters:
|
||||||
|
input_path_topic: /trajectory_guard/input_path
|
||||||
|
patched_path_topic: /trajectory_guard/patched_path
|
||||||
|
costmap_topic: /local_costmap/costmap
|
||||||
|
odom_topic: /odom_combined
|
||||||
|
planner_action: /compute_path_to_pose
|
||||||
|
follow_action: /follow_path
|
||||||
|
planner_id: GridBased
|
||||||
|
controller_id: FollowPath
|
||||||
|
goal_checker_id: ""
|
||||||
|
check_period_sec: 0.5
|
||||||
|
lookahead_distance: 2.0
|
||||||
|
rejoin_min_distance: 1.5
|
||||||
|
rejoin_max_distance: 5.0
|
||||||
|
occupied_threshold: 50
|
||||||
|
treat_unknown_as_occupied: true
|
||||||
|
footprint_half_length: 0.14
|
||||||
|
footprint_half_width: 0.085
|
||||||
|
footprint_padding: 0.04
|
||||||
|
footprint_sample_step: 0.05
|
||||||
|
repair_cooldown_sec: 0.5
|
||||||
|
blocked_retry_wait_sec: 0.8
|
||||||
|
wait_for_planner_costmap_update: true
|
||||||
|
min_repair_progress_indices: 5
|
||||||
|
publish_zero_on_blocked: true
|
||||||
|
cmd_vel_topic: /cmd_vel
|
||||||
@@ -24,6 +24,7 @@ from launch.actions import (
|
|||||||
DeclareLaunchArgument,
|
DeclareLaunchArgument,
|
||||||
GroupAction,
|
GroupAction,
|
||||||
IncludeLaunchDescription,
|
IncludeLaunchDescription,
|
||||||
|
SetEnvironmentVariable,
|
||||||
)
|
)
|
||||||
from launch.conditions import IfCondition
|
from launch.conditions import IfCondition
|
||||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||||
@@ -49,6 +50,7 @@ def generate_launch_description():
|
|||||||
start_obstacle_scanner = LaunchConfiguration('start_obstacle_scanner', default='true')
|
start_obstacle_scanner = LaunchConfiguration('start_obstacle_scanner', default='true')
|
||||||
|
|
||||||
nav2_param_path = os.path.join(pkg_dir, 'config', 'nav2_params.yaml')
|
nav2_param_path = os.path.join(pkg_dir, 'config', 'nav2_params.yaml')
|
||||||
|
fastdds_profile_path = os.path.join(pkg_dir, 'config', 'fastdds_udp_only.xml')
|
||||||
nav_to_pose_bt_path = os.path.join(
|
nav_to_pose_bt_path = os.path.join(
|
||||||
pkg_dir, 'behavior_tree', 'nav_to_pose_ackermann.xml')
|
pkg_dir, 'behavior_tree', 'nav_to_pose_ackermann.xml')
|
||||||
configured_params = RewrittenYaml(
|
configured_params = RewrittenYaml(
|
||||||
@@ -103,6 +105,7 @@ def generate_launch_description():
|
|||||||
'autostart': 'true',
|
'autostart': 'true',
|
||||||
}.items(),
|
}.items(),
|
||||||
)
|
)
|
||||||
|
|
||||||
# ========================== Assembly ======================================
|
# ========================== Assembly ======================================
|
||||||
return LaunchDescription([
|
return LaunchDescription([
|
||||||
DeclareLaunchArgument(
|
DeclareLaunchArgument(
|
||||||
@@ -138,6 +141,10 @@ def generate_launch_description():
|
|||||||
default_value='true',
|
default_value='true',
|
||||||
description='Also start the obstacle_scanner node'),
|
description='Also start the obstacle_scanner node'),
|
||||||
|
|
||||||
|
SetEnvironmentVariable(
|
||||||
|
name='FASTRTPS_DEFAULT_PROFILES_FILE',
|
||||||
|
value=fastdds_profile_path),
|
||||||
|
|
||||||
safe_base_bringup,
|
safe_base_bringup,
|
||||||
lslidar_launch,
|
lslidar_launch,
|
||||||
obstacle_scanner_launch,
|
obstacle_scanner_launch,
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
obstacle_nav2_q_waypoints.launch.py
|
||||||
|
====================================
|
||||||
|
复合启动文件:同时启动 obstacle_nav2 导航系统 + Q 路点候选选择器节点。
|
||||||
|
|
||||||
|
架构概览
|
||||||
|
--------
|
||||||
|
本 launch 文件包含两个核心组件:
|
||||||
|
1. obstacle_nav2 : 完整的障碍物导航栈(底盘驱动、激光雷达、Nav2 导航、障碍物扫描)
|
||||||
|
2. q_waypoint_selector : 独立的 Python 节点,周期性从 costmap 中评估并选择
|
||||||
|
最优的 Q 路点候选目标,通过 NavigateToPose action 下发导航目标。
|
||||||
|
|
||||||
|
Q 路点选择器的设计意图
|
||||||
|
----------------------
|
||||||
|
在比赛中,我们需要依次访问一系列路点(Q₁, Q₂, ..., Qₙ),但每个路点可能有多个
|
||||||
|
候选位置。选择器从 JSON 文件中加载有序路点及其候选坐标,结合全局 costmap 的
|
||||||
|
占用栅格信息,过滤掉被障碍物占据的候选点,并向 Nav2 下发当前最优的导航目标。
|
||||||
|
|
||||||
|
启动方式
|
||||||
|
--------
|
||||||
|
ros2 launch obstacle_nav2 obstacle_nav2_q_waypoints.launch.py \
|
||||||
|
use_sim_time:=false \
|
||||||
|
global_frame:=odom \
|
||||||
|
json_dir:=/path/to/json \
|
||||||
|
goal_frame:=odom \
|
||||||
|
costmap_topic:=/global_costmap/costmap
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from ament_index_python.packages import get_package_share_directory
|
||||||
|
from launch import LaunchDescription
|
||||||
|
from launch.actions import DeclareLaunchArgument, ExecuteProcess, IncludeLaunchDescription
|
||||||
|
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||||
|
from launch.substitutions import LaunchConfiguration
|
||||||
|
from launch.substitutions import FindExecutable
|
||||||
|
|
||||||
|
|
||||||
|
def generate_launch_description():
|
||||||
|
# =========================================================================
|
||||||
|
# 路径配置 —— 定位 obstacle_nav2 包内的关键文件
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
# obstacle_nav2 包的共享目录(install 后的 share/obstacle_nav2)
|
||||||
|
pkg_dir = get_package_share_directory("obstacle_nav2")
|
||||||
|
|
||||||
|
# 基础导航 launch 文件路径,将被 IncludeLaunchDescription 引入
|
||||||
|
base_launch = os.path.join(pkg_dir, "launch", "obstacle_nav2.launch.py")
|
||||||
|
|
||||||
|
# Q 路点选择器的 Python 脚本路径,将被 ExecuteProcess 作为独立进程运行
|
||||||
|
selector_script = os.path.join(pkg_dir, "config", "q_waypoint_selector.py")
|
||||||
|
|
||||||
|
# 默认的 JSON 路点文件目录
|
||||||
|
default_json_dir = os.path.join(pkg_dir, "config", "json")
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# LaunchConfiguration —— 运行时参数占位符
|
||||||
|
# 这些变量在 launch 时被替换为实际的参数值(命令行传入或默认值)。
|
||||||
|
# 使用 LaunchConfiguration 而非直接读取值,是为了支持 launch 系统的
|
||||||
|
# 延迟求值(lazy evaluation)机制。
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
# ---- obstacle_nav2 基础导航栈参数 ----
|
||||||
|
|
||||||
|
# 是否使用仿真时间(实车=false,Gazebo 仿真=true)
|
||||||
|
use_sim_time = LaunchConfiguration("use_sim_time")
|
||||||
|
|
||||||
|
# 全局坐标系名称(仿真用 "odom",实车用 "odom_combined")
|
||||||
|
global_frame = LaunchConfiguration("global_frame")
|
||||||
|
|
||||||
|
# 是否加载静态地图(已知地图导航=true,纯 SLAM 探索=false)
|
||||||
|
use_static_map = LaunchConfiguration("use_static_map")
|
||||||
|
|
||||||
|
# 静态地图 YAML 文件路径(为空时不加载 map_server)
|
||||||
|
map_yaml = LaunchConfiguration("map_yaml")
|
||||||
|
|
||||||
|
# 是否启用底盘运动控制(调试时可关闭以只运行感知)
|
||||||
|
enable_motion = LaunchConfiguration("enable_motion")
|
||||||
|
|
||||||
|
# 是否启动底盘驱动节点(实车=true,纯仿真可视化=false)
|
||||||
|
start_base = LaunchConfiguration("start_base")
|
||||||
|
|
||||||
|
# 是否启动激光雷达驱动节点
|
||||||
|
start_lidar = LaunchConfiguration("start_lidar")
|
||||||
|
|
||||||
|
# 是否启动障碍物扫描节点(obstacle_scanner)
|
||||||
|
start_obstacle_scanner = LaunchConfiguration("start_obstacle_scanner")
|
||||||
|
|
||||||
|
# ---- Q 路点选择器专属参数 ----
|
||||||
|
|
||||||
|
# JSON 路点文件所在目录,包含如 Q1.json, Q2.json 等有序路点定义
|
||||||
|
json_dir = LaunchConfiguration("json_dir")
|
||||||
|
|
||||||
|
# 下发 NavigateToPose 导航目标时使用的坐标系
|
||||||
|
goal_frame = LaunchConfiguration("goal_frame")
|
||||||
|
|
||||||
|
# 全局 costmap 话题,用于判断候选路点是否被障碍物占据
|
||||||
|
costmap_topic = LaunchConfiguration("costmap_topic")
|
||||||
|
|
||||||
|
# Nav2 的 NavigateToPose action 名称
|
||||||
|
navigate_action = LaunchConfiguration("navigate_action")
|
||||||
|
|
||||||
|
# 占用阈值:costmap 栅格值 >= 此值的单元格视为被占用(范围 0-100)
|
||||||
|
occupied_threshold = LaunchConfiguration("occupied_threshold")
|
||||||
|
|
||||||
|
# 是否将未知区域(costmap 值 == -1)视为占用
|
||||||
|
treat_unknown_as_occupied = LaunchConfiguration("treat_unknown_as_occupied")
|
||||||
|
|
||||||
|
# 单个候选目标的超时时间(秒),超时后取消当前目标并尝试下一个候选
|
||||||
|
goal_timeout_sec = LaunchConfiguration("goal_timeout_sec")
|
||||||
|
|
||||||
|
# 选择器的控制周期(秒),即多久重新评估一次最优候选路点
|
||||||
|
selection_period_sec = LaunchConfiguration("selection_period_sec")
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# 组件 1 —— obstacle_nav2 基础导航栈
|
||||||
|
# 通过 IncludeLaunchDescription 引入 obstacle_nav2.launch.py,
|
||||||
|
# 将其作为一个子 launch 嵌入当前 launch 描述中。
|
||||||
|
# 传入的参数会透传给子 launch 文件,实现参数的统一管理。
|
||||||
|
# =========================================================================
|
||||||
|
obstacle_nav2 = IncludeLaunchDescription(
|
||||||
|
PythonLaunchDescriptionSource(base_launch),
|
||||||
|
launch_arguments={
|
||||||
|
"use_sim_time": use_sim_time,
|
||||||
|
"global_frame": global_frame,
|
||||||
|
"use_static_map": use_static_map,
|
||||||
|
"map_yaml": map_yaml,
|
||||||
|
"enable_motion": enable_motion,
|
||||||
|
"start_base": start_base,
|
||||||
|
"start_lidar": start_lidar,
|
||||||
|
"start_obstacle_scanner": start_obstacle_scanner,
|
||||||
|
}.items(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# 组件 2 —— Q 路点候选选择器
|
||||||
|
# 作为一个独立的 Python 进程运行(ExecuteProcess),而非 ROS 2 Node action。
|
||||||
|
# 选择器内部会创建一个 rclpy 节点,订阅 costmap、发布导航目标。
|
||||||
|
# 参数通过 ROS 2 标准的 --ros-args -p 方式传递。
|
||||||
|
# =========================================================================
|
||||||
|
q_waypoint_selector = ExecuteProcess(
|
||||||
|
cmd=[
|
||||||
|
# 使用 FindExecutable 查找 python3 可执行文件,确保跨平台兼容
|
||||||
|
FindExecutable(name="python3"),
|
||||||
|
selector_script,
|
||||||
|
# ROS 2 命令行参数传递标准格式
|
||||||
|
"--ros-args",
|
||||||
|
"-p", ["json_dir:=", json_dir],
|
||||||
|
"-p", ["goal_frame:=", goal_frame],
|
||||||
|
"-p", ["costmap_topic:=", costmap_topic],
|
||||||
|
"-p", ["navigate_action:=", navigate_action],
|
||||||
|
"-p", ["occupied_threshold:=", occupied_threshold],
|
||||||
|
"-p", ["treat_unknown_as_occupied:=", treat_unknown_as_occupied],
|
||||||
|
"-p", ["goal_timeout_sec:=", goal_timeout_sec],
|
||||||
|
"-p", ["selection_period_sec:=", selection_period_sec],
|
||||||
|
"-p", ["use_sim_time:=", use_sim_time],
|
||||||
|
],
|
||||||
|
# 将进程的标准输出打印到终端,便于调试
|
||||||
|
output="screen",
|
||||||
|
emulate_tty=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# LaunchDescription —— 组装并返回完整的 launch 描述
|
||||||
|
# 包含三部分:
|
||||||
|
# 1. DeclareLaunchArgument —— 声明所有可配置参数及其默认值
|
||||||
|
# 2. obstacle_nav2 —— 基础导航栈
|
||||||
|
# 3. q_waypoint_selector —— Q 路点选择器
|
||||||
|
# =========================================================================
|
||||||
|
return LaunchDescription(
|
||||||
|
[
|
||||||
|
# =================================================================
|
||||||
|
# obstacle_nav2 基础导航栈参数声明
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
# 是否使用仿真时间(/clock 话题),实车场景应设为 false
|
||||||
|
DeclareLaunchArgument("use_sim_time", default_value="false"),
|
||||||
|
|
||||||
|
# 全局坐标系:仿真用 "odom",实车 EKF 融合后用 "odom_combined"
|
||||||
|
DeclareLaunchArgument("global_frame", default_value="odom"),
|
||||||
|
|
||||||
|
# 是否启用静态地图层(map_server),在线 SLAM 建图时设为 false
|
||||||
|
DeclareLaunchArgument("use_static_map", default_value="false"),
|
||||||
|
|
||||||
|
# 静态地图 yaml 文件路径,为空字符串时不加载地图
|
||||||
|
DeclareLaunchArgument("map_yaml", default_value=""),
|
||||||
|
|
||||||
|
# 是否启用底盘运动控制指令下发,调试时可关闭
|
||||||
|
DeclareLaunchArgument("enable_motion", default_value="true"),
|
||||||
|
|
||||||
|
# 是否启动底盘串口驱动节点(实车需要,仿真不需要)
|
||||||
|
DeclareLaunchArgument("start_base", default_value="true"),
|
||||||
|
|
||||||
|
# 是否启动激光雷达驱动节点
|
||||||
|
DeclareLaunchArgument("start_lidar", default_value="true"),
|
||||||
|
|
||||||
|
# 是否启动障碍物扫描与避障节点
|
||||||
|
DeclareLaunchArgument("start_obstacle_scanner", default_value="true"),
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# Q 路点选择器参数声明
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
# JSON 路点文件目录,每个 JSON 文件定义一个路点的候选坐标列表
|
||||||
|
DeclareLaunchArgument(
|
||||||
|
"json_dir",
|
||||||
|
default_value=default_json_dir,
|
||||||
|
description="Directory containing ordered waypoint JSON files.",
|
||||||
|
),
|
||||||
|
|
||||||
|
# 导航目标使用的坐标帧,应与全局 costmap 的坐标系一致
|
||||||
|
DeclareLaunchArgument(
|
||||||
|
"goal_frame",
|
||||||
|
default_value="odom",
|
||||||
|
description="Frame used for NavigateToPose goals.",
|
||||||
|
),
|
||||||
|
|
||||||
|
# 全局 costmap 话题,选择器订阅此话题来评估候选点的可通行性
|
||||||
|
DeclareLaunchArgument(
|
||||||
|
"costmap_topic",
|
||||||
|
default_value="/global_costmap/costmap",
|
||||||
|
description="OccupancyGrid topic used to reject occupied candidates.",
|
||||||
|
),
|
||||||
|
|
||||||
|
# NavigateToPose action 的服务名,与 Nav2 的 bt_navigator 对应
|
||||||
|
DeclareLaunchArgument(
|
||||||
|
"navigate_action",
|
||||||
|
default_value="/navigate_to_pose",
|
||||||
|
description="Nav2 NavigateToPose action name.",
|
||||||
|
),
|
||||||
|
|
||||||
|
# costmap 占用判定阈值(0-100),>= 此值的栅格视为障碍物
|
||||||
|
DeclareLaunchArgument(
|
||||||
|
"occupied_threshold",
|
||||||
|
default_value="50",
|
||||||
|
description="Costmap cells >= this value are considered occupied.",
|
||||||
|
),
|
||||||
|
|
||||||
|
# 是否将未知区域(costmap 值 == -1)视为不可通行
|
||||||
|
DeclareLaunchArgument(
|
||||||
|
"treat_unknown_as_occupied",
|
||||||
|
default_value="true",
|
||||||
|
description="Treat costmap value -1 as occupied.",
|
||||||
|
),
|
||||||
|
|
||||||
|
# 单个导航目标的超时时间,超时后自动取消并切换候选
|
||||||
|
DeclareLaunchArgument(
|
||||||
|
"goal_timeout_sec",
|
||||||
|
default_value="60.0",
|
||||||
|
description="Cancel a candidate goal if it does not finish before this timeout.",
|
||||||
|
),
|
||||||
|
|
||||||
|
# 选择器主循环的控制周期(秒),值越小响应越快但 CPU 开销越大
|
||||||
|
DeclareLaunchArgument(
|
||||||
|
"selection_period_sec",
|
||||||
|
default_value="0.5",
|
||||||
|
description="Selector control period.",
|
||||||
|
),
|
||||||
|
|
||||||
|
# ---- 组装已配置的 action ----
|
||||||
|
obstacle_nav2,
|
||||||
|
q_waypoint_selector,
|
||||||
|
]
|
||||||
|
)
|
||||||
277
src/navigation/obstacle_nav2/src/trajectory_guard.cpp
Normal file
277
src/navigation/obstacle_nav2/src/trajectory_guard.cpp
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
#include "obstacle_nav2/trajectory_guard.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace obstacle_nav2
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
bool worldToMap(
|
||||||
|
const nav_msgs::msg::OccupancyGrid & grid,
|
||||||
|
double wx,
|
||||||
|
double wy,
|
||||||
|
int & mx,
|
||||||
|
int & my)
|
||||||
|
{
|
||||||
|
const double resolution = grid.info.resolution;
|
||||||
|
if (resolution <= 0.0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double ox = grid.info.origin.position.x;
|
||||||
|
const double oy = grid.info.origin.position.y;
|
||||||
|
if (wx < ox || wy < oy) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mx = static_cast<int>((wx - ox) / resolution);
|
||||||
|
my = static_cast<int>((wy - oy) / resolution);
|
||||||
|
return mx >= 0 && my >= 0 &&
|
||||||
|
mx < static_cast<int>(grid.info.width) &&
|
||||||
|
my < static_cast<int>(grid.info.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool occupiedAt(
|
||||||
|
const nav_msgs::msg::OccupancyGrid & grid,
|
||||||
|
double wx,
|
||||||
|
double wy,
|
||||||
|
const GuardSettings & settings)
|
||||||
|
{
|
||||||
|
int mx = 0;
|
||||||
|
int my = 0;
|
||||||
|
if (!worldToMap(grid, wx, wy, mx, my)) {
|
||||||
|
return settings.treat_unknown_as_occupied;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto value = grid.data[my * grid.info.width + mx];
|
||||||
|
if (value < 0) {
|
||||||
|
return settings.treat_unknown_as_occupied;
|
||||||
|
}
|
||||||
|
return value >= settings.occupied_threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool footprintOccupied(
|
||||||
|
const nav_msgs::msg::OccupancyGrid & costmap,
|
||||||
|
double cx,
|
||||||
|
double cy,
|
||||||
|
double yaw,
|
||||||
|
const GuardSettings & settings)
|
||||||
|
{
|
||||||
|
const double half_l = settings.footprint_half_length + settings.footprint_padding;
|
||||||
|
const double half_w = settings.footprint_half_width + settings.footprint_padding;
|
||||||
|
const double step = std::max(0.02, settings.footprint_sample_step);
|
||||||
|
const double c = std::cos(yaw);
|
||||||
|
const double s = std::sin(yaw);
|
||||||
|
|
||||||
|
auto sample = [&](double x, double y) {
|
||||||
|
const double wx = cx + c * x - s * y;
|
||||||
|
const double wy = cy + s * x + c * y;
|
||||||
|
return occupiedAt(costmap, wx, wy, settings);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sample(0.0, 0.0)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (double x = -half_l; x <= half_l + 1.0e-9; x += step) {
|
||||||
|
if (sample(x, -half_w) || sample(x, 0.0) || sample(x, half_w)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (double y = -half_w; y <= half_w + 1.0e-9; y += step) {
|
||||||
|
if (sample(-half_l, y) || sample(0.0, y) || sample(half_l, y)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
double yawFromPose(const geometry_msgs::msg::PoseStamped & pose)
|
||||||
|
{
|
||||||
|
const auto & q = pose.pose.orientation;
|
||||||
|
return std::atan2(
|
||||||
|
2.0 * (q.w * q.z + q.x * q.y),
|
||||||
|
1.0 - 2.0 * (q.y * q.y + q.z * q.z));
|
||||||
|
}
|
||||||
|
|
||||||
|
double distance2d(
|
||||||
|
const geometry_msgs::msg::PoseStamped & a,
|
||||||
|
const geometry_msgs::msg::PoseStamped & b)
|
||||||
|
{
|
||||||
|
const double dx = a.pose.position.x - b.pose.position.x;
|
||||||
|
const double dy = a.pose.position.y - b.pose.position.y;
|
||||||
|
return std::hypot(dx, dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t nearestPathIndex(
|
||||||
|
const nav_msgs::msg::Path & path,
|
||||||
|
const geometry_msgs::msg::PoseStamped & pose)
|
||||||
|
{
|
||||||
|
std::size_t best_index = 0;
|
||||||
|
double best_distance = std::numeric_limits<double>::infinity();
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < path.poses.size(); ++i) {
|
||||||
|
const double d = distance2d(path.poses[i], pose);
|
||||||
|
if (d < best_distance) {
|
||||||
|
best_distance = d;
|
||||||
|
best_index = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t advanceByDistance(
|
||||||
|
const nav_msgs::msg::Path & path,
|
||||||
|
std::size_t start_index,
|
||||||
|
double distance_m)
|
||||||
|
{
|
||||||
|
if (path.poses.empty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t index = std::min(start_index, path.poses.size() - 1);
|
||||||
|
double traveled = 0.0;
|
||||||
|
while (index + 1 < path.poses.size() && traveled < distance_m) {
|
||||||
|
traveled += distance2d(path.poses[index], path.poses[index + 1]);
|
||||||
|
++index;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
CollisionCheckResult checkPathAhead(
|
||||||
|
const nav_msgs::msg::Path & path,
|
||||||
|
std::size_t start_index,
|
||||||
|
const nav_msgs::msg::OccupancyGrid & costmap,
|
||||||
|
const GuardSettings & settings)
|
||||||
|
{
|
||||||
|
if (path.poses.empty()) {
|
||||||
|
return CollisionCheckResult{true, 0, "empty_path"};
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto clamped_start = std::min(start_index, path.poses.size() - 1);
|
||||||
|
const auto end_index =
|
||||||
|
advanceByDistance(path, clamped_start, settings.lookahead_distance);
|
||||||
|
const double step = std::max(0.02, settings.footprint_sample_step);
|
||||||
|
|
||||||
|
for (std::size_t i = clamped_start; i <= end_index && i < path.poses.size(); ++i) {
|
||||||
|
if (i + 1 >= path.poses.size() || i == end_index) {
|
||||||
|
const auto & pose = path.poses[i];
|
||||||
|
if (footprintOccupied(
|
||||||
|
costmap, pose.pose.position.x, pose.pose.position.y, yawFromPose(pose), settings))
|
||||||
|
{
|
||||||
|
return CollisionCheckResult{true, i, "occupied"};
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto & a = path.poses[i];
|
||||||
|
const auto & b = path.poses[i + 1];
|
||||||
|
const double dx = b.pose.position.x - a.pose.position.x;
|
||||||
|
const double dy = b.pose.position.y - a.pose.position.y;
|
||||||
|
const double segment_length = std::hypot(dx, dy);
|
||||||
|
const double yaw = segment_length > 1.0e-6 ? std::atan2(dy, dx) : yawFromPose(a);
|
||||||
|
const int samples = std::max(1, static_cast<int>(std::ceil(segment_length / step)));
|
||||||
|
|
||||||
|
for (int sample_index = 0; sample_index <= samples; ++sample_index) {
|
||||||
|
const double t = static_cast<double>(sample_index) / static_cast<double>(samples);
|
||||||
|
const double x = a.pose.position.x + t * dx;
|
||||||
|
const double y = a.pose.position.y + t * dy;
|
||||||
|
if (footprintOccupied(costmap, x, y, yaw, settings)) {
|
||||||
|
return CollisionCheckResult{true, i, "occupied"};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CollisionCheckResult{false, end_index, "clear"};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::size_t> findClearRejoinIndex(
|
||||||
|
const nav_msgs::msg::Path & path,
|
||||||
|
std::size_t start_index,
|
||||||
|
const nav_msgs::msg::OccupancyGrid & costmap,
|
||||||
|
const GuardSettings & settings)
|
||||||
|
{
|
||||||
|
if (path.poses.empty()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto first = advanceByDistance(path, start_index, settings.rejoin_min_distance);
|
||||||
|
const auto last = advanceByDistance(path, start_index, settings.rejoin_max_distance);
|
||||||
|
GuardSettings single_pose_settings = settings;
|
||||||
|
single_pose_settings.lookahead_distance = 0.0;
|
||||||
|
|
||||||
|
for (std::size_t i = first; i <= last && i < path.poses.size(); ++i) {
|
||||||
|
const auto result = checkPathAhead(path, i, costmap, single_pose_settings);
|
||||||
|
if (!result.blocked) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shouldRetryBlockedRepair(
|
||||||
|
const std::optional<std::size_t> & last_repair_nearest_index,
|
||||||
|
std::size_t nearest_index,
|
||||||
|
int min_repair_progress_indices,
|
||||||
|
double seconds_since_last_repair,
|
||||||
|
double blocked_retry_wait_sec,
|
||||||
|
bool costmap_updated_since_repair)
|
||||||
|
{
|
||||||
|
if (!last_repair_nearest_index.has_value()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto min_progress =
|
||||||
|
static_cast<std::size_t>(std::max(0, min_repair_progress_indices));
|
||||||
|
if (nearest_index > *last_repair_nearest_index + min_progress) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return seconds_since_last_repair >= blocked_retry_wait_sec &&
|
||||||
|
costmap_updated_since_repair;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav_msgs::msg::Path slicePath(
|
||||||
|
const nav_msgs::msg::Path & path,
|
||||||
|
std::size_t start_index,
|
||||||
|
std::size_t end_index_inclusive)
|
||||||
|
{
|
||||||
|
nav_msgs::msg::Path out;
|
||||||
|
out.header = path.header;
|
||||||
|
if (path.poses.empty()) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto begin = std::min(start_index, path.poses.size() - 1);
|
||||||
|
const auto end = std::min(end_index_inclusive, path.poses.size() - 1);
|
||||||
|
for (std::size_t i = begin; i <= end; ++i) {
|
||||||
|
out.poses.push_back(path.poses[i]);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav_msgs::msg::Path stitchPaths(
|
||||||
|
const nav_msgs::msg::Path & bypass,
|
||||||
|
const nav_msgs::msg::Path & original,
|
||||||
|
std::size_t rejoin_index)
|
||||||
|
{
|
||||||
|
nav_msgs::msg::Path out;
|
||||||
|
out.header = bypass.header.frame_id.empty() ? original.header : bypass.header;
|
||||||
|
out.poses = bypass.poses;
|
||||||
|
|
||||||
|
if (original.poses.empty()) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto begin = std::min(rejoin_index, original.poses.size());
|
||||||
|
for (std::size_t i = begin; i < original.poses.size(); ++i) {
|
||||||
|
out.poses.push_back(original.poses[i]);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace obstacle_nav2
|
||||||
454
src/navigation/obstacle_nav2/src/trajectory_guard_node.cpp
Normal file
454
src/navigation/obstacle_nav2/src/trajectory_guard_node.cpp
Normal file
@@ -0,0 +1,454 @@
|
|||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "geometry_msgs/msg/pose_stamped.hpp"
|
||||||
|
#include "geometry_msgs/msg/twist.hpp"
|
||||||
|
#include "nav2_msgs/action/compute_path_to_pose.hpp"
|
||||||
|
#include "nav2_msgs/action/follow_path.hpp"
|
||||||
|
#include "nav_msgs/msg/occupancy_grid.hpp"
|
||||||
|
#include "nav_msgs/msg/odometry.hpp"
|
||||||
|
#include "nav_msgs/msg/path.hpp"
|
||||||
|
#include "obstacle_nav2/trajectory_guard.hpp"
|
||||||
|
#include "rclcpp/rclcpp.hpp"
|
||||||
|
#include "rclcpp_action/rclcpp_action.hpp"
|
||||||
|
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
namespace obstacle_nav2
|
||||||
|
{
|
||||||
|
|
||||||
|
class TrajectoryGuardNode : public rclcpp::Node
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using ComputePathToPose = nav2_msgs::action::ComputePathToPose;
|
||||||
|
using FollowPath = nav2_msgs::action::FollowPath;
|
||||||
|
using ComputeGoalHandle = rclcpp_action::ClientGoalHandle<ComputePathToPose>;
|
||||||
|
using FollowGoalHandle = rclcpp_action::ClientGoalHandle<FollowPath>;
|
||||||
|
|
||||||
|
TrajectoryGuardNode()
|
||||||
|
: Node("trajectory_guard_node")
|
||||||
|
{
|
||||||
|
declare_parameter("input_path_topic", "/trajectory_guard/input_path");
|
||||||
|
declare_parameter("patched_path_topic", "/trajectory_guard/patched_path");
|
||||||
|
declare_parameter("costmap_topic", "/local_costmap/costmap");
|
||||||
|
declare_parameter("planner_costmap_topic", "/global_costmap/costmap");
|
||||||
|
declare_parameter("odom_topic", "/odom_combined");
|
||||||
|
declare_parameter("planner_action", "/compute_path_to_pose");
|
||||||
|
declare_parameter("follow_action", "/follow_path");
|
||||||
|
declare_parameter("planner_id", "GridBased");
|
||||||
|
declare_parameter("controller_id", "FollowPath");
|
||||||
|
declare_parameter("goal_checker_id", "");
|
||||||
|
declare_parameter("check_period_sec", 0.5);
|
||||||
|
declare_parameter("lookahead_distance", 2.0);
|
||||||
|
declare_parameter("rejoin_min_distance", 1.0);
|
||||||
|
declare_parameter("rejoin_max_distance", 4.0);
|
||||||
|
declare_parameter("occupied_threshold", 50);
|
||||||
|
declare_parameter("treat_unknown_as_occupied", true);
|
||||||
|
declare_parameter("footprint_half_length", 0.14);
|
||||||
|
declare_parameter("footprint_half_width", 0.085);
|
||||||
|
declare_parameter("footprint_padding", 0.04);
|
||||||
|
declare_parameter("footprint_sample_step", 0.05);
|
||||||
|
declare_parameter("repair_cooldown_sec", 1.0);
|
||||||
|
declare_parameter("blocked_retry_wait_sec", 0.8);
|
||||||
|
declare_parameter("wait_for_planner_costmap_update", true);
|
||||||
|
declare_parameter("min_repair_progress_indices", 5);
|
||||||
|
declare_parameter("publish_zero_on_blocked", true);
|
||||||
|
declare_parameter("cmd_vel_topic", "/cmd_vel");
|
||||||
|
declare_parameter("execute_follow_path", true);
|
||||||
|
|
||||||
|
loadParameters();
|
||||||
|
last_repair_request_time_ =
|
||||||
|
get_clock()->now() - rclcpp::Duration::from_seconds(repair_cooldown_sec_);
|
||||||
|
|
||||||
|
path_sub_ = create_subscription<nav_msgs::msg::Path>(
|
||||||
|
get_parameter("input_path_topic").as_string(), 1,
|
||||||
|
[this](nav_msgs::msg::Path::SharedPtr msg) { onPath(msg); });
|
||||||
|
costmap_sub_ = create_subscription<nav_msgs::msg::OccupancyGrid>(
|
||||||
|
get_parameter("costmap_topic").as_string(), 1,
|
||||||
|
[this](nav_msgs::msg::OccupancyGrid::SharedPtr msg) {
|
||||||
|
latest_costmap_ = msg;
|
||||||
|
++local_costmap_generation_;
|
||||||
|
});
|
||||||
|
planner_costmap_sub_ = create_subscription<nav_msgs::msg::OccupancyGrid>(
|
||||||
|
get_parameter("planner_costmap_topic").as_string(), 1,
|
||||||
|
[this](nav_msgs::msg::OccupancyGrid::SharedPtr msg) {
|
||||||
|
latest_planner_costmap_ = msg;
|
||||||
|
++planner_costmap_generation_;
|
||||||
|
});
|
||||||
|
odom_sub_ = create_subscription<nav_msgs::msg::Odometry>(
|
||||||
|
get_parameter("odom_topic").as_string(), 10,
|
||||||
|
[this](nav_msgs::msg::Odometry::SharedPtr msg) { latest_odom_ = msg; });
|
||||||
|
|
||||||
|
patched_path_pub_ = create_publisher<nav_msgs::msg::Path>(
|
||||||
|
get_parameter("patched_path_topic").as_string(), 1);
|
||||||
|
cmd_vel_pub_ = create_publisher<geometry_msgs::msg::Twist>(
|
||||||
|
get_parameter("cmd_vel_topic").as_string(), 1);
|
||||||
|
|
||||||
|
planner_client_ = rclcpp_action::create_client<ComputePathToPose>(
|
||||||
|
this, get_parameter("planner_action").as_string());
|
||||||
|
follow_client_ = rclcpp_action::create_client<FollowPath>(
|
||||||
|
this, get_parameter("follow_action").as_string());
|
||||||
|
|
||||||
|
const auto period = std::chrono::duration<double>(
|
||||||
|
std::max(0.1, get_parameter("check_period_sec").as_double()));
|
||||||
|
timer_ = create_wall_timer(
|
||||||
|
std::chrono::duration_cast<std::chrono::milliseconds>(period),
|
||||||
|
[this]() { tick(); });
|
||||||
|
|
||||||
|
RCLCPP_INFO(
|
||||||
|
get_logger(), "trajectory guard ready check_period_sec=%.2f input=%s patched=%s",
|
||||||
|
get_parameter("check_period_sec").as_double(),
|
||||||
|
get_parameter("input_path_topic").as_string().c_str(),
|
||||||
|
get_parameter("patched_path_topic").as_string().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void loadParameters()
|
||||||
|
{
|
||||||
|
settings_.lookahead_distance = get_parameter("lookahead_distance").as_double();
|
||||||
|
settings_.rejoin_min_distance = get_parameter("rejoin_min_distance").as_double();
|
||||||
|
settings_.rejoin_max_distance = get_parameter("rejoin_max_distance").as_double();
|
||||||
|
settings_.occupied_threshold = get_parameter("occupied_threshold").as_int();
|
||||||
|
settings_.treat_unknown_as_occupied =
|
||||||
|
get_parameter("treat_unknown_as_occupied").as_bool();
|
||||||
|
settings_.footprint_half_length = get_parameter("footprint_half_length").as_double();
|
||||||
|
settings_.footprint_half_width = get_parameter("footprint_half_width").as_double();
|
||||||
|
settings_.footprint_padding = get_parameter("footprint_padding").as_double();
|
||||||
|
settings_.footprint_sample_step = get_parameter("footprint_sample_step").as_double();
|
||||||
|
repair_cooldown_sec_ = std::max(0.0, get_parameter("repair_cooldown_sec").as_double());
|
||||||
|
blocked_retry_wait_sec_ =
|
||||||
|
std::max(0.0, get_parameter("blocked_retry_wait_sec").as_double());
|
||||||
|
wait_for_planner_costmap_update_ =
|
||||||
|
get_parameter("wait_for_planner_costmap_update").as_bool();
|
||||||
|
min_repair_progress_indices_ =
|
||||||
|
std::max(0, static_cast<int>(get_parameter("min_repair_progress_indices").as_int()));
|
||||||
|
publish_zero_on_blocked_ = get_parameter("publish_zero_on_blocked").as_bool();
|
||||||
|
execute_follow_path_ = get_parameter("execute_follow_path").as_bool();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onPath(const nav_msgs::msg::Path::SharedPtr msg)
|
||||||
|
{
|
||||||
|
active_path_ = *msg;
|
||||||
|
sent_original_for_current_path_ = false;
|
||||||
|
last_repair_nearest_index_.reset();
|
||||||
|
last_repair_rejoin_index_.reset();
|
||||||
|
retry_without_planner_costmap_update_ = false;
|
||||||
|
waiting_for_planner_costmap_update_ = false;
|
||||||
|
RCLCPP_INFO(
|
||||||
|
get_logger(), "received replay path poses=%zu frame=%s",
|
||||||
|
active_path_.poses.size(), active_path_.header.frame_id.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<geometry_msgs::msg::PoseStamped> currentPose() const
|
||||||
|
{
|
||||||
|
if (!latest_odom_) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
geometry_msgs::msg::PoseStamped pose;
|
||||||
|
pose.header = latest_odom_->header;
|
||||||
|
pose.pose = latest_odom_->pose.pose;
|
||||||
|
return pose;
|
||||||
|
}
|
||||||
|
|
||||||
|
void tick()
|
||||||
|
{
|
||||||
|
if (active_path_.poses.empty() || !latest_costmap_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto pose = currentPose();
|
||||||
|
if (!pose.has_value()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto nearest = nearestPathIndex(active_path_, *pose);
|
||||||
|
const auto result = checkPathAhead(active_path_, nearest, *latest_costmap_, settings_);
|
||||||
|
if (!result.blocked) {
|
||||||
|
if (!sent_original_for_current_path_) {
|
||||||
|
publishAndMaybeFollow(active_path_, "original_clear");
|
||||||
|
sent_original_for_current_path_ = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto now = get_clock()->now();
|
||||||
|
if (repair_in_flight_ ||
|
||||||
|
(now - last_repair_request_time_).seconds() < repair_cooldown_sec_)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!waiting_for_planner_costmap_update_) {
|
||||||
|
waiting_for_planner_costmap_update_ = true;
|
||||||
|
waiting_planner_costmap_generation_ = planner_costmap_generation_;
|
||||||
|
RCLCPP_WARN_THROTTLE(
|
||||||
|
get_logger(), *get_clock(), 2000,
|
||||||
|
"blocked replay path; waiting for next planner costmap frame before requesting bypass");
|
||||||
|
publishStop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (planner_costmap_generation_ <= waiting_planner_costmap_generation_) {
|
||||||
|
if (retry_without_planner_costmap_update_ &&
|
||||||
|
(now - last_repair_request_time_).seconds() >= blocked_retry_wait_sec_)
|
||||||
|
{
|
||||||
|
waiting_for_planner_costmap_update_ = false;
|
||||||
|
} else {
|
||||||
|
RCLCPP_WARN_THROTTLE(
|
||||||
|
get_logger(), *get_clock(), 2000,
|
||||||
|
"blocked replay path; planner costmap has not updated yet");
|
||||||
|
publishStop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
waiting_for_planner_costmap_update_ = false;
|
||||||
|
|
||||||
|
const bool costmap_updated_since_repair =
|
||||||
|
!wait_for_planner_costmap_update_ ||
|
||||||
|
planner_costmap_generation_ > last_repair_planner_costmap_generation_;
|
||||||
|
const bool timeout_retry_allowed =
|
||||||
|
retry_without_planner_costmap_update_ &&
|
||||||
|
(now - last_repair_request_time_).seconds() >= blocked_retry_wait_sec_;
|
||||||
|
if (!shouldRetryBlockedRepair(
|
||||||
|
last_repair_nearest_index_, nearest, min_repair_progress_indices_,
|
||||||
|
(now - last_repair_request_time_).seconds(), blocked_retry_wait_sec_,
|
||||||
|
costmap_updated_since_repair) && !timeout_retry_allowed)
|
||||||
|
{
|
||||||
|
RCLCPP_WARN_THROTTLE(
|
||||||
|
get_logger(), *get_clock(), 2000,
|
||||||
|
"patched path still blocked near the last repair point; waiting for path progress or costmap update");
|
||||||
|
publishStop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
retry_without_planner_costmap_update_ = false;
|
||||||
|
|
||||||
|
std::size_t search_start = nearest;
|
||||||
|
if (last_repair_rejoin_index_.has_value()) {
|
||||||
|
const auto bump = static_cast<std::size_t>(std::max(1, min_repair_progress_indices_));
|
||||||
|
const auto advanced = std::min(
|
||||||
|
active_path_.poses.size() - 1, *last_repair_rejoin_index_ + bump);
|
||||||
|
search_start = std::max(search_start, advanced);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto rejoin = findClearRejoinIndex(
|
||||||
|
active_path_, search_start, *latest_costmap_, settings_);
|
||||||
|
if (!rejoin.has_value()) {
|
||||||
|
RCLCPP_WARN(get_logger(), "blocked replay path but no clear rejoin point found");
|
||||||
|
publishStop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RCLCPP_WARN(
|
||||||
|
get_logger(), "blocked replay path index=%zu reason=%s rejoin_index=%zu",
|
||||||
|
result.path_index, result.reason.c_str(), *rejoin);
|
||||||
|
requestBypass(nearest, *rejoin);
|
||||||
|
}
|
||||||
|
|
||||||
|
void publishAndMaybeFollow(const nav_msgs::msg::Path & path, const std::string & label)
|
||||||
|
{
|
||||||
|
if (path.poses.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav_msgs::msg::Path stamped_path = path;
|
||||||
|
stamped_path.header.stamp = now();
|
||||||
|
for (auto & pose : stamped_path.poses) {
|
||||||
|
pose.header.stamp = stamped_path.header.stamp;
|
||||||
|
if (pose.header.frame_id.empty()) {
|
||||||
|
pose.header.frame_id = stamped_path.header.frame_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
patched_path_pub_->publish(stamped_path);
|
||||||
|
|
||||||
|
if (!execute_follow_path_) {
|
||||||
|
RCLCPP_INFO(
|
||||||
|
get_logger(), "published %s path poses=%zu without FollowPath execution",
|
||||||
|
label.c_str(), stamped_path.poses.size());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendFollowPath(stamped_path, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendFollowPath(const nav_msgs::msg::Path & path, const std::string & label)
|
||||||
|
{
|
||||||
|
if (!follow_client_->wait_for_action_server(100ms)) {
|
||||||
|
RCLCPP_WARN(get_logger(), "FollowPath action server not ready");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (active_follow_goal_handle_) {
|
||||||
|
follow_client_->async_cancel_goal(active_follow_goal_handle_);
|
||||||
|
active_follow_goal_handle_.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
FollowPath::Goal goal;
|
||||||
|
goal.path = path;
|
||||||
|
goal.controller_id = get_parameter("controller_id").as_string();
|
||||||
|
goal.goal_checker_id = get_parameter("goal_checker_id").as_string();
|
||||||
|
|
||||||
|
auto options = rclcpp_action::Client<FollowPath>::SendGoalOptions();
|
||||||
|
options.goal_response_callback =
|
||||||
|
[this, label](FollowGoalHandle::SharedPtr goal_handle) {
|
||||||
|
if (!goal_handle) {
|
||||||
|
RCLCPP_WARN(get_logger(), "FollowPath %s rejected", label.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
active_follow_goal_handle_ = goal_handle;
|
||||||
|
RCLCPP_INFO(get_logger(), "FollowPath %s accepted", label.c_str());
|
||||||
|
};
|
||||||
|
options.result_callback =
|
||||||
|
[this, label](const FollowGoalHandle::WrappedResult & result) {
|
||||||
|
active_follow_goal_handle_.reset();
|
||||||
|
RCLCPP_INFO(
|
||||||
|
get_logger(), "FollowPath %s finished code=%d",
|
||||||
|
label.c_str(), static_cast<int>(result.code));
|
||||||
|
};
|
||||||
|
|
||||||
|
follow_client_->async_send_goal(goal, options);
|
||||||
|
RCLCPP_INFO(get_logger(), "sent FollowPath %s poses=%zu", label.c_str(), path.poses.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void requestBypass(std::size_t nearest_index, std::size_t rejoin_index)
|
||||||
|
{
|
||||||
|
const auto start = currentPose();
|
||||||
|
if (!start.has_value() || rejoin_index >= active_path_.poses.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!planner_client_->wait_for_action_server(100ms)) {
|
||||||
|
RCLCPP_WARN(get_logger(), "ComputePathToPose action server not ready");
|
||||||
|
publishStop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
repair_in_flight_ = true;
|
||||||
|
last_repair_request_time_ = get_clock()->now();
|
||||||
|
last_repair_nearest_index_ = nearest_index;
|
||||||
|
last_repair_rejoin_index_ = rejoin_index;
|
||||||
|
last_repair_planner_costmap_generation_ = planner_costmap_generation_;
|
||||||
|
waiting_for_planner_costmap_update_ = false;
|
||||||
|
|
||||||
|
ComputePathToPose::Goal goal;
|
||||||
|
goal.start = *start;
|
||||||
|
goal.goal = active_path_.poses[rejoin_index];
|
||||||
|
goal.planner_id = get_parameter("planner_id").as_string();
|
||||||
|
goal.use_start = true;
|
||||||
|
|
||||||
|
auto options = rclcpp_action::Client<ComputePathToPose>::SendGoalOptions();
|
||||||
|
options.goal_response_callback =
|
||||||
|
[this](ComputeGoalHandle::SharedPtr goal_handle) {
|
||||||
|
if (!goal_handle) {
|
||||||
|
repair_in_flight_ = false;
|
||||||
|
RCLCPP_WARN(get_logger(), "bypass planning goal rejected");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
options.result_callback =
|
||||||
|
[this, rejoin_index](const ComputeGoalHandle::WrappedResult & result) {
|
||||||
|
repair_in_flight_ = false;
|
||||||
|
if (result.code != rclcpp_action::ResultCode::SUCCEEDED ||
|
||||||
|
result.result->path.poses.empty())
|
||||||
|
{
|
||||||
|
RCLCPP_WARN(get_logger(), "bypass planning failed code=%d", static_cast<int>(result.code));
|
||||||
|
publishStop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto patched = stitchPaths(result.result->path, active_path_, rejoin_index);
|
||||||
|
if (!patchedPathIsClear(patched)) {
|
||||||
|
retry_without_planner_costmap_update_ = true;
|
||||||
|
waiting_for_planner_costmap_update_ = false;
|
||||||
|
RCLCPP_WARN(
|
||||||
|
get_logger(),
|
||||||
|
"planned bypass still blocked in local costmap; stopping and retrying from a later rejoin point");
|
||||||
|
publishStop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
active_path_ = patched;
|
||||||
|
sent_original_for_current_path_ = true;
|
||||||
|
waiting_for_planner_costmap_update_ = false;
|
||||||
|
retry_without_planner_costmap_update_ = false;
|
||||||
|
publishAndMaybeFollow(patched, "patched_bypass");
|
||||||
|
};
|
||||||
|
|
||||||
|
planner_client_->async_send_goal(goal, options);
|
||||||
|
RCLCPP_INFO(get_logger(), "requested bypass to rejoin_index=%zu", rejoin_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool patchedPathIsClear(const nav_msgs::msg::Path & patched) const
|
||||||
|
{
|
||||||
|
if (!latest_costmap_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto pose = currentPose();
|
||||||
|
if (!pose.has_value()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto nearest = nearestPathIndex(patched, *pose);
|
||||||
|
const auto result = checkPathAhead(patched, nearest, *latest_costmap_, settings_);
|
||||||
|
return !result.blocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
void publishStop()
|
||||||
|
{
|
||||||
|
if (!publish_zero_on_blocked_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
geometry_msgs::msg::Twist stop;
|
||||||
|
cmd_vel_pub_->publish(stop);
|
||||||
|
}
|
||||||
|
|
||||||
|
GuardSettings settings_;
|
||||||
|
double repair_cooldown_sec_{1.0};
|
||||||
|
double blocked_retry_wait_sec_{0.8};
|
||||||
|
int min_repair_progress_indices_{5};
|
||||||
|
bool publish_zero_on_blocked_{true};
|
||||||
|
bool execute_follow_path_{true};
|
||||||
|
bool wait_for_planner_costmap_update_{true};
|
||||||
|
bool retry_without_planner_costmap_update_{false};
|
||||||
|
bool repair_in_flight_{false};
|
||||||
|
bool sent_original_for_current_path_{false};
|
||||||
|
bool waiting_for_planner_costmap_update_{false};
|
||||||
|
rclcpp::Time last_repair_request_time_{0, 0, RCL_ROS_TIME};
|
||||||
|
std::optional<std::size_t> last_repair_nearest_index_;
|
||||||
|
std::optional<std::size_t> last_repair_rejoin_index_;
|
||||||
|
std::uint64_t local_costmap_generation_{0};
|
||||||
|
std::uint64_t planner_costmap_generation_{0};
|
||||||
|
std::uint64_t last_repair_planner_costmap_generation_{0};
|
||||||
|
std::uint64_t waiting_planner_costmap_generation_{0};
|
||||||
|
|
||||||
|
nav_msgs::msg::Path active_path_;
|
||||||
|
nav_msgs::msg::OccupancyGrid::SharedPtr latest_costmap_;
|
||||||
|
nav_msgs::msg::OccupancyGrid::SharedPtr latest_planner_costmap_;
|
||||||
|
nav_msgs::msg::Odometry::SharedPtr latest_odom_;
|
||||||
|
FollowGoalHandle::SharedPtr active_follow_goal_handle_;
|
||||||
|
|
||||||
|
rclcpp::Subscription<nav_msgs::msg::Path>::SharedPtr path_sub_;
|
||||||
|
rclcpp::Subscription<nav_msgs::msg::OccupancyGrid>::SharedPtr costmap_sub_;
|
||||||
|
rclcpp::Subscription<nav_msgs::msg::OccupancyGrid>::SharedPtr planner_costmap_sub_;
|
||||||
|
rclcpp::Subscription<nav_msgs::msg::Odometry>::SharedPtr odom_sub_;
|
||||||
|
rclcpp::Publisher<nav_msgs::msg::Path>::SharedPtr patched_path_pub_;
|
||||||
|
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr cmd_vel_pub_;
|
||||||
|
rclcpp_action::Client<ComputePathToPose>::SharedPtr planner_client_;
|
||||||
|
rclcpp_action::Client<FollowPath>::SharedPtr follow_client_;
|
||||||
|
rclcpp::TimerBase::SharedPtr timer_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace obstacle_nav2
|
||||||
|
|
||||||
|
int main(int argc, char ** argv)
|
||||||
|
{
|
||||||
|
rclcpp::init(argc, argv);
|
||||||
|
rclcpp::spin(std::make_shared<obstacle_nav2::TrajectoryGuardNode>());
|
||||||
|
rclcpp::shutdown();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
162
src/navigation/obstacle_nav2/test/test_trajectory_guard.cpp
Normal file
162
src/navigation/obstacle_nav2/test/test_trajectory_guard.cpp
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "obstacle_nav2/trajectory_guard.hpp"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
geometry_msgs::msg::PoseStamped pose(double x, double y)
|
||||||
|
{
|
||||||
|
geometry_msgs::msg::PoseStamped p;
|
||||||
|
p.header.frame_id = "odom";
|
||||||
|
p.pose.position.x = x;
|
||||||
|
p.pose.position.y = y;
|
||||||
|
p.pose.orientation.w = 1.0;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav_msgs::msg::Path straightPath()
|
||||||
|
{
|
||||||
|
nav_msgs::msg::Path path;
|
||||||
|
path.header.frame_id = "odom";
|
||||||
|
for (int i = 0; i <= 10; ++i) {
|
||||||
|
path.poses.push_back(pose(0.2 * i, 0.0));
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav_msgs::msg::OccupancyGrid gridWithOrigin(double origin_x, double origin_y)
|
||||||
|
{
|
||||||
|
nav_msgs::msg::OccupancyGrid grid;
|
||||||
|
grid.header.frame_id = "odom";
|
||||||
|
grid.info.resolution = 0.1;
|
||||||
|
grid.info.width = 40;
|
||||||
|
grid.info.height = 30;
|
||||||
|
grid.info.origin.position.x = origin_x;
|
||||||
|
grid.info.origin.position.y = origin_y;
|
||||||
|
grid.data.assign(grid.info.width * grid.info.height, 0);
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
void markOccupied(nav_msgs::msg::OccupancyGrid & grid, int mx, int my)
|
||||||
|
{
|
||||||
|
ASSERT_GE(mx, 0);
|
||||||
|
ASSERT_GE(my, 0);
|
||||||
|
ASSERT_LT(mx, static_cast<int>(grid.info.width));
|
||||||
|
ASSERT_LT(my, static_cast<int>(grid.info.height));
|
||||||
|
grid.data[my * grid.info.width + mx] = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(TrajectoryGuard, NearestPathIndexFindsClosestPose)
|
||||||
|
{
|
||||||
|
const auto path = straightPath();
|
||||||
|
EXPECT_EQ(obstacle_nav2::nearestPathIndex(path, pose(0.43, 0.02)), 2u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(TrajectoryGuard, AdvanceByDistanceStopsAtRequestedArcLength)
|
||||||
|
{
|
||||||
|
const auto path = straightPath();
|
||||||
|
EXPECT_EQ(obstacle_nav2::advanceByDistance(path, 0, 0.55), 3u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(TrajectoryGuard, SlicePathIncludesEndIndex)
|
||||||
|
{
|
||||||
|
const auto path = straightPath();
|
||||||
|
const auto sliced = obstacle_nav2::slicePath(path, 2, 4);
|
||||||
|
ASSERT_EQ(sliced.poses.size(), 3u);
|
||||||
|
EXPECT_DOUBLE_EQ(sliced.poses.front().pose.position.x, 0.4);
|
||||||
|
EXPECT_DOUBLE_EQ(sliced.poses.back().pose.position.x, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(TrajectoryGuard, StitchPathsAppendsOriginalFromRejoinIndex)
|
||||||
|
{
|
||||||
|
const auto original = straightPath();
|
||||||
|
nav_msgs::msg::Path bypass;
|
||||||
|
bypass.header.frame_id = "odom";
|
||||||
|
bypass.poses.push_back(pose(0.0, 0.0));
|
||||||
|
bypass.poses.push_back(pose(0.5, 0.3));
|
||||||
|
|
||||||
|
const auto stitched = obstacle_nav2::stitchPaths(bypass, original, 4);
|
||||||
|
ASSERT_EQ(stitched.poses.size(), 9u);
|
||||||
|
EXPECT_DOUBLE_EQ(stitched.poses[0].pose.position.y, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(stitched.poses[1].pose.position.y, 0.3);
|
||||||
|
EXPECT_DOUBLE_EQ(stitched.poses[2].pose.position.x, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(TrajectoryGuard, CheckPathAheadReportsOccupiedCell)
|
||||||
|
{
|
||||||
|
auto path = straightPath();
|
||||||
|
auto grid = gridWithOrigin(-1.0, -1.0);
|
||||||
|
markOccupied(grid, 15, 10);
|
||||||
|
|
||||||
|
obstacle_nav2::GuardSettings settings;
|
||||||
|
settings.lookahead_distance = 2.0;
|
||||||
|
settings.occupied_threshold = 50;
|
||||||
|
settings.footprint_half_length = 0.01;
|
||||||
|
settings.footprint_half_width = 0.01;
|
||||||
|
settings.footprint_padding = 0.0;
|
||||||
|
|
||||||
|
const auto result = obstacle_nav2::checkPathAhead(path, 0, grid, settings);
|
||||||
|
EXPECT_TRUE(result.blocked);
|
||||||
|
EXPECT_EQ(result.reason, "occupied");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(TrajectoryGuard, CheckPathAheadKeepsClearPathUnblocked)
|
||||||
|
{
|
||||||
|
auto path = straightPath();
|
||||||
|
auto grid = gridWithOrigin(-1.0, -1.0);
|
||||||
|
|
||||||
|
obstacle_nav2::GuardSettings settings;
|
||||||
|
settings.lookahead_distance = 2.0;
|
||||||
|
settings.occupied_threshold = 50;
|
||||||
|
settings.footprint_half_length = 0.01;
|
||||||
|
settings.footprint_half_width = 0.01;
|
||||||
|
settings.footprint_padding = 0.0;
|
||||||
|
|
||||||
|
const auto result = obstacle_nav2::checkPathAhead(path, 0, grid, settings);
|
||||||
|
EXPECT_FALSE(result.blocked);
|
||||||
|
EXPECT_EQ(result.reason, "clear");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(TrajectoryGuard, FindClearRejoinIndexSkipsBlockedArea)
|
||||||
|
{
|
||||||
|
auto path = straightPath();
|
||||||
|
auto grid = gridWithOrigin(-1.0, -1.0);
|
||||||
|
for (int mx = 13; mx <= 18; ++mx) {
|
||||||
|
markOccupied(grid, mx, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
obstacle_nav2::GuardSettings settings;
|
||||||
|
settings.rejoin_min_distance = 0.5;
|
||||||
|
settings.rejoin_max_distance = 2.0;
|
||||||
|
settings.occupied_threshold = 50;
|
||||||
|
settings.footprint_half_length = 0.01;
|
||||||
|
settings.footprint_half_width = 0.01;
|
||||||
|
settings.footprint_padding = 0.0;
|
||||||
|
|
||||||
|
const auto rejoin = obstacle_nav2::findClearRejoinIndex(path, 0, grid, settings);
|
||||||
|
ASSERT_TRUE(rejoin.has_value());
|
||||||
|
EXPECT_GE(*rejoin, 5u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(TrajectoryGuard, RetryBlockedRepairWaitsForCostmapUpdateAndRetryDelay)
|
||||||
|
{
|
||||||
|
const std::optional<std::size_t> last_repair_index = 10u;
|
||||||
|
|
||||||
|
EXPECT_FALSE(obstacle_nav2::shouldRetryBlockedRepair(
|
||||||
|
last_repair_index, 11u, 5, 0.8, 1.0, true));
|
||||||
|
EXPECT_FALSE(obstacle_nav2::shouldRetryBlockedRepair(
|
||||||
|
last_repair_index, 11u, 5, 1.2, 1.0, false));
|
||||||
|
EXPECT_TRUE(obstacle_nav2::shouldRetryBlockedRepair(
|
||||||
|
last_repair_index, 11u, 5, 1.2, 1.0, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(TrajectoryGuard, RetryBlockedRepairAllowsProgressedPathImmediately)
|
||||||
|
{
|
||||||
|
const std::optional<std::size_t> last_repair_index = 10u;
|
||||||
|
|
||||||
|
EXPECT_TRUE(obstacle_nav2::shouldRetryBlockedRepair(
|
||||||
|
last_repair_index, 16u, 5, 0.1, 1.0, false));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user