From 9d16b632e484dcb07a51468f691542861ceafad3 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Thu, 13 Aug 2026 18:17:27 +0200 Subject: [PATCH 1/3] build(ota_demo): declare the plugin gtest ROS-free for the domain gate The suite drives the catalog client, the operation dispatcher and the plugin against doubles and never creates a ROS entity, so it takes no DDS domain. Say so at the registration site, which is what the per-package domain allocation gate reads. Guarded on the target, because ament_add_gtest registers nothing when the executable was not created, and setting a property on a test that does not exist is a configure error rather than the skip ament intended. --- .../ota_update_plugin/CMakeLists.txt | 12 +++++++++ .../config/nav2_params.yaml | 26 ++++++++----------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/demos/ota_nav2_sensor_fix/ota_update_plugin/CMakeLists.txt b/demos/ota_nav2_sensor_fix/ota_update_plugin/CMakeLists.txt index 050ca09..ba7618a 100644 --- a/demos/ota_nav2_sensor_fix/ota_update_plugin/CMakeLists.txt +++ b/demos/ota_nav2_sensor_fix/ota_update_plugin/CMakeLists.txt @@ -74,6 +74,7 @@ install(DIRECTORY include/ DESTINATION include) if(BUILD_TESTING) find_package(ament_cmake_gtest REQUIRED) + include(ROS2MedkitTestDomain) ament_add_gtest(test_ota_update_plugin test/test_operation_dispatcher.cpp test/test_catalog_client.cpp @@ -81,6 +82,17 @@ if(BUILD_TESTING) ) target_link_libraries(test_ota_update_plugin ota_update_plugin_core) target_include_directories(test_ota_update_plugin PRIVATE src) + # The suite creates no ROS entity: the catalog client, the operation + # dispatcher and the plugin are exercised against doubles, and the one test + # that uses the real ProcessRunner spawns a path that does not exist. Nothing + # here touches DDS, so the test needs no domain of its own. + # + # Guarded, because ament_add_gtest registers nothing when the executable was + # not created, and declaring a property on a test that does not exist is a + # hard configure error rather than the skip ament intended. + if(TARGET test_ota_update_plugin) + medkit_test_needs_no_domain(test_ota_update_plugin) + endif() endif() ament_package() diff --git a/demos/ota_nav2_sensor_fix/ros2_packages/ota_nav2_sensor_fix_demo/config/nav2_params.yaml b/demos/ota_nav2_sensor_fix/ros2_packages/ota_nav2_sensor_fix_demo/config/nav2_params.yaml index ad5e816..2330674 100644 --- a/demos/ota_nav2_sensor_fix/ros2_packages/ota_nav2_sensor_fix_demo/config/nav2_params.yaml +++ b/demos/ota_nav2_sensor_fix/ros2_packages/ota_nav2_sensor_fix_demo/config/nav2_params.yaml @@ -233,21 +233,17 @@ global_costmap: robot_radius: 0.45 resolution: 0.05 track_unknown_space: true - plugins: ["static_layer", "obstacle_layer", "inflation_layer"] - 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 - raytrace_min_range: 0.0 - obstacle_max_range: 2.5 - obstacle_min_range: 0.0 + # Map-only, no obstacle layer: the global plan comes from the static map + # plus inflation, and reacting to what the lidar sees is the local + # costmap's job. The warehouse is static, so the map already holds every + # obstacle a global plan has to route around. It also decides WHERE the + # AMR fails when the lidar regresses: the phantom sector is fixed to the + # robot, so a global obstacle layer would paint it around the robot's own + # position and navfn would abort with "failed to create plan" before the + # controller ever ran out of trajectories. The failure belongs at the + # controller that cannot find a way forward, which is the node the log + # bridge watches. + plugins: ["static_layer", "inflation_layer"] static_layer: plugin: "nav2_costmap_2d::StaticLayer" map_subscribe_transient_local: True From c741a3c547221e8ffb1e0b31a7656203eb0b66a9 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Thu, 13 Aug 2026 18:17:27 +0200 Subject: [PATCH 2/3] fix(ota_demo): keep the phantom out of the global costmap so the controller is what fails The phantom sector the regressed lidar overlays is fixed to the robot and reports a constant 0.22 m, so the global costmap painted it around the robot's own position and navfn gave up before the controller ran out of trajectories. Which of the two failed first depended on the geometry at onset, and the log bridge watches controller_server, not planner_server, so the supporting LOG_ fault on controller-server appeared or did not. The global costmap now ignores scan returns below 0.35 m. The phantom never reaches the planner, real obstacles still do - which matters, because the committed map is 62% unknown and eleven warehouse models stand outside it, so global planning cannot fall back on the static map alone. Nav2 fails where the README says it does: the controller cannot make progress, logs it, and navigate_to_pose aborts. --- .../config/nav2_params.yaml | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/demos/ota_nav2_sensor_fix/ros2_packages/ota_nav2_sensor_fix_demo/config/nav2_params.yaml b/demos/ota_nav2_sensor_fix/ros2_packages/ota_nav2_sensor_fix_demo/config/nav2_params.yaml index 2330674..721d9b7 100644 --- a/demos/ota_nav2_sensor_fix/ros2_packages/ota_nav2_sensor_fix_demo/config/nav2_params.yaml +++ b/demos/ota_nav2_sensor_fix/ros2_packages/ota_nav2_sensor_fix_demo/config/nav2_params.yaml @@ -233,17 +233,33 @@ global_costmap: robot_radius: 0.45 resolution: 0.05 track_unknown_space: true - # Map-only, no obstacle layer: the global plan comes from the static map - # plus inflation, and reacting to what the lidar sees is the local - # costmap's job. The warehouse is static, so the map already holds every - # obstacle a global plan has to route around. It also decides WHERE the - # AMR fails when the lidar regresses: the phantom sector is fixed to the - # robot, so a global obstacle layer would paint it around the robot's own - # position and navfn would abort with "failed to create plan" before the - # controller ever ran out of trajectories. The failure belongs at the - # controller that cannot find a way forward, which is the node the log - # bridge watches. - plugins: ["static_layer", "inflation_layer"] + plugins: ["static_layer", "obstacle_layer", "inflation_layer"] + 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 + raytrace_min_range: 0.0 + obstacle_max_range: 2.5 + # Global planning ignores anything closer than this, and that decides + # WHICH Nav2 node fails when the lidar regresses. The phantom sector + # broken_lidar overlays is fixed to the robot and reports a constant + # range (phantom_range_m, 0.22 m at the laser), so without this floor + # the global costmap paints it around the robot's own position and + # navfn aborts with "failed to create plan" before the controller + # runs out of trajectories - and the log bridge watches + # controller_server, not planner_server, so the supporting fault + # would go missing. The floor must therefore stay above + # phantom_range_m and below the distance at which real obstacles + # matter to a global plan; anything that close is the local + # costmap's problem, and it reads the same scan with no floor. + obstacle_min_range: 0.35 static_layer: plugin: "nav2_costmap_2d::StaticLayer" map_subscribe_transient_local: True From 13dda69497b0d9b4d9cf624041f161064fdac26e Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Thu, 13 Aug 2026 18:17:27 +0200 Subject: [PATCH 3/3] test(ota_demo): assert the supporting fault is the controller stall, not any error The check was "controller-server has at least one fault". The log bridge promotes every controller_server ERROR above its severity floor, so a TF error or a lifecycle error passed it just as well, and nothing in the suite could tell "the controller stalled on the phantom" from "the controller logged something". Match the fault message against the two errors the controller raises when it cannot move: the progress checker and the controller patience. A goal that aborts anywhere else in Nav2 now fails this assertion instead of sliding through on an unrelated error. --- tests/smoke_test_demo_narrative.sh | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/tests/smoke_test_demo_narrative.sh b/tests/smoke_test_demo_narrative.sh index e42700a..1167b28 100755 --- a/tests/smoke_test_demo_narrative.sh +++ b/tests/smoke_test_demo_narrative.sh @@ -29,7 +29,8 @@ # scan_sensor_node is running broken_lidar_node; fixed_lidar_3_0_1 is # NOT yet registered (boot catalog holds only the bad update). # 2. send-goal.sh -> ACTION_NAVIGATE_TO_POSE_ABORTED reaches CONFIRMED on -# bt-navigator, and controller-server picks up a supporting LOG_* fault. +# bt-navigator, and controller-server picks up a supporting LOG_* fault +# whose message is the controller's own stall, not just any error. # 3. Fault detail (bt-navigator) has environment_data.snapshots >= 1, and # the rosbag bulk-data download returns a non-empty MCAP body. # 4. publish-fix.sh -> fixed_lidar_3_0_1 appears in /updates (SOVD @@ -68,6 +69,17 @@ CONTROLLER_ENTITY="apps/controller-server" # controller-server's LOG_CONTROLLER_SERVER_* code is content-hashed (derived # from the log message), so it is never matched by exact code - only by # "does this entity have any fault at all" (see fault_present with code=""). +# +# The message is matched instead, because "any fault on controller-server" is +# too weak for the one assertion that says WHICH Nav2 node failed. The log +# bridge promotes every controller_server ERROR at or above its severity floor, +# so a TF error or a lifecycle error would satisfy a bare count check. These two +# messages are the controller saying it cannot move: the progress checker +# (movement_time_allowance) and the controller patience (failure_tolerance). +# If the global costmap ever starts marking the phantom again, planner_server +# aborts the goal first, controller_server logs neither of these, and this is +# the assertion that goes red. +CONTROLLER_STALL_MSG="Failed to make progress|Controller patience exceeded" # --- Helpers built on top of smoke_lib.sh's api_get/poll_until ------------- @@ -231,12 +243,14 @@ else "fault never reached CONFIRMED within 60s - either nav2 didn't accept the goal or the action-status bridge is broken" fi -echo " Waiting for a supporting LOG_* fault on ${CONTROLLER_ENTITY} (max 60s)..." -if poll_until "/${CONTROLLER_ENTITY}/faults" '.items | length > 0' 60; then - pass "supporting LOG_* fault present on ${CONTROLLER_ENTITY}" +echo " Waiting for a supporting LOG_* stall fault on ${CONTROLLER_ENTITY} (max 60s)..." +if poll_until "/${CONTROLLER_ENTITY}/faults" \ + ".items[] | select(.description | test(\"${CONTROLLER_STALL_MSG}\"))" \ + 60; then + pass "supporting LOG_* fault present on ${CONTROLLER_ENTITY} and reports the controller stall" else - fail "supporting LOG_* fault present on ${CONTROLLER_ENTITY}" \ - "no fault appeared within 60s - either nav2 didn't stall or the log bridge is broken" + fail "supporting LOG_* fault present on ${CONTROLLER_ENTITY} and reports the controller stall" \ + "no fault matching '${CONTROLLER_STALL_MSG}' within 60s - either nav2 aborted somewhere other than the controller, or the log bridge is broken" fi # ---------------------------------------------------------------------