From 32da10bb7b14c0db4490ba333489d97cd81a3728 Mon Sep 17 00:00:00 2001 From: Breelyn Styler Date: Tue, 28 Jul 2026 19:06:11 -0400 Subject: [PATCH 1/2] feat(hangar_sim): fuse odometry localization for the mobile base Give hangar_sim's mobile base a realistic localization stack so beluga_amcl is exercised against fuse-fused odometry (wheel + IMU) with real drift, including where the scene is degenerate for scan matching (the smooth fuselage, the unmapped picking boxes). - Fuse on by default (use_fuse=true) -> /odom_filtered; odom_world_drift injects a live odom->world from the fuse estimate so AMCL sees drift to correct while world->base stays ground truth for whole-body planning. - slip_aware_odom (new C++ node): grows wheel-yaw covariance during sustained in-place spin (mecanum roller slip) so fuse defers to the IMU while spinning and trusts the wheels driving straight; also re-anchors odom across the whole-body<->nav controller handoff. - AMCL tuning: OmniMotionModel; alpha1 0.1->0.4; update_min_a 0.1 with resample_interval 3; likelihood relaxed (sigma_hit 0.1->0.25, z_hit 0.9->0.65, z_rand 0.1->0.3) so unmapped boxes read as outliers instead of yanking the pose. - fuse lag_duration 0.5->0.3: shorter smoother window cuts output latency (moving-yaw 0.70->0.465deg) while still smoothing transient spikes. - amcl_odom_gate (new C++ node): sole map->odom publisher (AMCL tf_broadcast false). Holds the last good map->odom and coasts on fuse odom where AMCL is degenerate, blending back when trustworthy. A large correction is accepted only if it persists over a sliding window (gated on position AND yaw) with particle-spread hysteresis; spread_accept_max additionally rejects a confident-but-WRONG lock (which persists yet stays spread) so a scan-slide divergence is coasted through while a real recovery (which converges) is still adopted. latency_compensation_sec composes the correction with odom->base from one estimator-lag ago (referenced to the cloud stamp), forward-projecting it with real buffered odometry -> moving-pose error 13.8->8.2cm, moving-yaw 1.2->0.9deg, no overshoot on turns; 0 disables, hangar_sim uses 0.30. The pure decision logic (detail::updateGate, interpolateOdom, appendOdomSample) has no ROS/TF deps and is fully unit-tested. - Removed the per-objective SetInitialPose reseed from the clicked-point Objectives (superseded by slip_aware_odom re-anchoring; the unconditional reseed could cement a drifting estimate). - wz_max 0.6 (below the velocity_smoother cap so it binds) keeps spins within AMCL's correction bandwidth; odom_rate 50Hz / tf_publish_rate 30Hz and the broadcaster rates decoupled from the control loop give the stack CPU headroom. Closes #19667. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/hangar_sim/CMakeLists.txt | 36 ++ .../control/picknik_ur.ros2_control.yaml | 2 + src/hangar_sim/config/fuse/fuse.yaml | 6 +- .../picknik_ur_mujoco_ros2_control.xacro | 4 +- src/hangar_sim/description/ur5e_ridgeback.xml | 4 + .../hangar_sim/amcl_odom_gate_logic.hpp | 186 ++++++ .../hangar_sim/odom_world_drift_logic.hpp | 76 +++ src/hangar_sim/include/hangar_sim/se2.hpp | 61 ++ .../robot_drivers_to_persist_sim.launch.py | 132 +++- .../objectives/navigate_to_clicked_point.xml | 1 - ...igate_to_clicked_point_with_replanning.xml | 1 - src/hangar_sim/package.xml | 9 +- src/hangar_sim/params/nav2_params.yaml | 46 +- src/hangar_sim/src/amcl_odom_gate.cpp | 348 +++++++++++ src/hangar_sim/src/amcl_odom_gate_logic.cpp | 113 ++++ src/hangar_sim/src/odom_world_drift.cpp | 174 ++++++ src/hangar_sim/src/slip_aware_odom.cpp | 206 +++++++ src/hangar_sim/test/test_amcl_odom_gate.cpp | 583 ++++++++++++++++++ src/hangar_sim/test/test_odom_world_drift.cpp | 104 ++++ 19 files changed, 2058 insertions(+), 34 deletions(-) create mode 100644 src/hangar_sim/include/hangar_sim/amcl_odom_gate_logic.hpp create mode 100644 src/hangar_sim/include/hangar_sim/odom_world_drift_logic.hpp create mode 100644 src/hangar_sim/include/hangar_sim/se2.hpp create mode 100644 src/hangar_sim/src/amcl_odom_gate.cpp create mode 100644 src/hangar_sim/src/amcl_odom_gate_logic.cpp create mode 100644 src/hangar_sim/src/odom_world_drift.cpp create mode 100644 src/hangar_sim/src/slip_aware_odom.cpp create mode 100644 src/hangar_sim/test/test_amcl_odom_gate.cpp create mode 100644 src/hangar_sim/test/test_odom_world_drift.cpp diff --git a/src/hangar_sim/CMakeLists.txt b/src/hangar_sim/CMakeLists.txt index 0d49770e5..c055f0155 100644 --- a/src/hangar_sim/CMakeLists.txt +++ b/src/hangar_sim/CMakeLists.txt @@ -3,6 +3,31 @@ project(hangar_sim) find_package(ament_cmake REQUIRED) find_package(picknik_accessories REQUIRED) +find_package(rclcpp REQUIRED) +find_package(nav_msgs REQUIRED) +find_package(geometry_msgs REQUIRED) +find_package(sensor_msgs REQUIRED) +find_package(tf2 REQUIRED) +find_package(tf2_geometry_msgs REQUIRED) +find_package(tf2_ros REQUIRED) + +add_executable(slip_aware_odom src/slip_aware_odom.cpp) +target_include_directories(slip_aware_odom PRIVATE include) +target_compile_features(slip_aware_odom PRIVATE cxx_std_20) +ament_target_dependencies(slip_aware_odom rclcpp nav_msgs tf2 tf2_geometry_msgs) + +add_executable(amcl_odom_gate src/amcl_odom_gate.cpp src/amcl_odom_gate_logic.cpp) +target_include_directories(amcl_odom_gate PRIVATE include) +target_compile_features(amcl_odom_gate PRIVATE cxx_std_20) +ament_target_dependencies(amcl_odom_gate rclcpp geometry_msgs tf2 tf2_geometry_msgs tf2_ros) + +add_executable(odom_world_drift src/odom_world_drift.cpp) +target_include_directories(odom_world_drift PRIVATE include) +target_compile_features(odom_world_drift PRIVATE cxx_std_20) +ament_target_dependencies(odom_world_drift rclcpp nav_msgs sensor_msgs geometry_msgs tf2 tf2_geometry_msgs + tf2_ros) + +install(TARGETS slip_aware_odom amcl_odom_gate odom_world_drift DESTINATION lib/${PROJECT_NAME}) install( DIRECTORY @@ -47,6 +72,17 @@ if(BUILD_TESTING) ENV MOVEIT_CONFIG_PACKAGE=hangar_sim MOVEIT_HOST_USER_WORKSPACE=${CMAKE_SOURCE_DIR} ROS_LOG_DIR=${CMAKE_CURRENT_BINARY_DIR}/test_results/${PROJECT_NAME}/ros_logs) + + # Unit tests for the pure amcl_odom_gate decision logic (no ROS/TF). + find_package(ament_cmake_gmock REQUIRED) + ament_add_gmock(test_amcl_odom_gate test/test_amcl_odom_gate.cpp src/amcl_odom_gate_logic.cpp) + target_include_directories(test_amcl_odom_gate PRIVATE include) + target_compile_features(test_amcl_odom_gate PRIVATE cxx_std_20) + + # Unit tests for the pure odom_world_drift rail-joint index resolver (no ROS). + ament_add_gmock(test_odom_world_drift test/test_odom_world_drift.cpp) + target_include_directories(test_odom_world_drift PRIVATE include) + target_compile_features(test_odom_world_drift PRIVATE cxx_std_20) endif() ament_package() diff --git a/src/hangar_sim/config/control/picknik_ur.ros2_control.yaml b/src/hangar_sim/config/control/picknik_ur.ros2_control.yaml index ab6e0afb3..a6446f8a1 100644 --- a/src/hangar_sim/config/control/picknik_ur.ros2_control.yaml +++ b/src/hangar_sim/config/control/picknik_ur.ros2_control.yaml @@ -160,6 +160,7 @@ platform_velocity_controller_nav2: joint_state_broadcaster: ros__parameters: use_local_topics: false + update_rate: 50 # publish /joint_states at 50 Hz, not the 600 Hz control loop joints: - shoulder_pan_joint - shoulder_lift_joint @@ -297,6 +298,7 @@ force_torque_sensor_broadcaster: imu_sensor_broadcaster: ros__parameters: + update_rate: 100 # 100 Hz IMU is plenty for fuse (optimizes at 10 Hz); was inheriting the 600 Hz control loop sensor_name: imu_site frame_id: ridgeback_base_link # Static covariance values (row-major 3x3 matrices) diff --git a/src/hangar_sim/config/fuse/fuse.yaml b/src/hangar_sim/config/fuse/fuse.yaml index 4ad1039f5..22f5135b6 100644 --- a/src/hangar_sim/config/fuse/fuse.yaml +++ b/src/hangar_sim/config/fuse/fuse.yaml @@ -3,9 +3,9 @@ state_estimator: ros__parameters: # Fixed-lag smoother configuration - optimization_frequency: 20.0 + optimization_frequency: 10.0 # match publish_frequency (10 Hz); optimizing 2x faster than we publish was wasted work transaction_timeout: 0.01 - lag_duration: 0.5 + lag_duration: 0.3 # chosen: median 0.465deg (<0.5 goal), clean transient tail vs 0.25 # Motion model for mobile base (3D omnidirectional) motion_models: @@ -51,7 +51,7 @@ state_estimator: # only the relative change between consecutive messages, preventing # accumulated drift from corrupting the estimate. wheel_odom_sensor: - topic: /platform_velocity_controller_nav2/odom + topic: /odom_slip_aware # slip_aware_odom republishes with spin-aware yaw covariance queue_size: 10 pose_loss: type: fuse_loss::HuberLoss diff --git a/src/hangar_sim/description/picknik_ur_mujoco_ros2_control.xacro b/src/hangar_sim/description/picknik_ur_mujoco_ros2_control.xacro index 42241108e..05ba84c3f 100644 --- a/src/hangar_sim/description/picknik_ur_mujoco_ros2_control.xacro +++ b/src/hangar_sim/description/picknik_ur_mujoco_ros2_control.xacro @@ -31,11 +31,11 @@ ${mujoco_model} hangar_sim 20 - 60 + 30 10 ${publish_odom} ridgeback_base_link - 150 + 50 diff --git a/src/hangar_sim/description/ur5e_ridgeback.xml b/src/hangar_sim/description/ur5e_ridgeback.xml index 2c18a2d13..5fe0538e3 100644 --- a/src/hangar_sim/description/ur5e_ridgeback.xml +++ b/src/hangar_sim/description/ur5e_ridgeback.xml @@ -313,6 +313,10 @@ conaffinity="0" group="2" /> + +