Easynav recovery - #119
Open
fmrico wants to merge 10 commits into
Open
Easynav recovery#119fmrico wants to merge 10 commits into
fmrico wants to merge 10 commits into
Conversation
Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical concurrency and exception-safety defects, plus plugin and dependency issues, block approval.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds EasyNav’s two-level recovery architecture with real-time safety reflexes, non-RT recovery orchestration, control ownership, diagnostics, mission escalation, tests, and documentation.
Changes:
- Adds recovery/reflex interfaces, plugin loading, arbitration, and lifecycle integration.
- Moves collision handling into a safety-reflex plugin.
- Adds TUI diagnostics, packaging updates, tests, and recovery documentation.
File summaries
| File | Reviewed change and final findings |
|---|---|
easynav/package.xml |
Adds the recovery dependency. Moderate (2 votes): recovery is not included in the metapackage’s exported CMake dependencies. |
easynav_tools/package.xml |
Adds tool dependencies. Critical (3 votes): diagnostic_msgs is imported but not declared directly. |
easynav_tools/easynav_tools/tui/app.py |
Adds diagnostics and mitigation panels. |
easynav_tools/easynav_tools/controller/ros_controllers.py |
Adds diagnostic message handling. Moderate (3 votes): all DEBUG logs are treated as the manager marker, hiding legitimate reports. |
easynav_system/tests/system_safety_reflex_tests.cpp |
Tests safety-reflex loading. |
easynav_system/tests/CMakeLists.txt |
Registers safety-reflex tests. |
easynav_system/src/easynav_system/SystemNode.cpp |
Integrates recovery and RT reflex cycles. Critical (3 votes): control_owner access can race between RT and non-RT threads. Moderate (1 vote): destructor may read undeclared safety_reflex_types. |
easynav_system/src/easynav_system/GoalManager.cpp |
Handles mission cancellation escalation. Moderate (2 votes): diagnostic has() access is unsynchronized. |
easynav_system/package.xml |
Adds recovery and diagnostics dependencies. |
easynav_system/include/easynav_system/SystemNode.hpp |
Declares recovery and reflex members. |
easynav_system/include/easynav_system/GoalManager.hpp |
Adjusts the height tolerance default. |
easynav_system/CMakeLists.txt |
Builds and links recovery components. Moderate (1 vote): diagnostic_msgs is not linked or exported directly. |
easynav_sensors/src/easynav_sensors/types/PointPerception.cpp |
Throttles TF failure logging. |
easynav_recovery/tests/recovery_manager_node_tests.cpp |
Tests recovery manager behavior. |
easynav_recovery/tests/CMakeLists.txt |
Registers recovery tests. |
easynav_recovery/src/easynav_recovery/RecoveryManagerNode.cpp |
Implements recovery orchestration. Critical (3 votes): RT/non-RT arbitration state is unsynchronized. Critical (3 votes): can_handle() is not exception-safe. Critical (2 votes): requires_control() can throw on the RT path. Critical (2 votes): diagnostic has() access races with updates. Moderate (2 votes): completed non-control mitigations can be repeatedly selected. Moderate (1 vote): control mitigation success is reported without validating a fresh command. Moderate (1 vote): destructor can read undeclared mitigation_types or evaluator_types. Moderate (1 vote): mitigation report lookup is unsynchronized. |
easynav_recovery/src/easynav_recovery/DummyMitigation.cpp |
Adds a test mitigation plugin. |
easynav_recovery/src/easynav_recovery/DummyEvaluator.cpp |
Adds a test evaluator plugin. |
easynav_recovery/package.xml |
Defines recovery package dependencies. |
easynav_recovery/include/easynav_recovery/RecoveryManagerNode.hpp |
Declares the recovery manager API and state. |
easynav_recovery/include/easynav_recovery/DummyMitigation.hpp |
Declares the dummy mitigation. |
easynav_recovery/include/easynav_recovery/DummyEvaluator.hpp |
Declares the dummy evaluator. |
easynav_recovery/easynav_recovery_plugins.xml |
Registers recovery plugins. Moderate (1 vote each): advertised evaluator and mitigation catalogs are not present or registered. |
easynav_recovery/CMakeLists.txt |
Builds and exports recovery libraries. |
easynav_core/test/obstacle_proximity_tests.cpp |
Tests obstacle proximity calculations. |
easynav_core/test/core_method_test.cpp |
Tests recovery interfaces and exception handling. |
easynav_core/test/CMakeLists.txt |
Registers core tests. |
easynav_core/src/easynav_core/SafetyReflexBase.cpp |
Implements safety-reflex handling. Critical (1 vote): diagnostics read-modify-write operations can lose reflex entries. |
easynav_core/src/easynav_core/RecoveryMitigationBase.cpp |
Implements mitigation lifecycle and reporting. Critical (1 vote): failed on_start() can still leave a mitigation active and take control ownership. |
easynav_core/src/easynav_core/RecoveryEvaluatorBase.cpp |
Implements evaluator execution and diagnostics. |
easynav_core/src/easynav_core/PlannerMethodBase.cpp |
Adds planner exception handling. |
easynav_core/src/easynav_core/ObstacleProximity.cpp |
Implements proximity computation. Critical (1 vote): perception fusion can mutate shared RT state and race sensor access. |
easynav_core/src/easynav_core/MapsManagerBase.cpp |
Adds map-manager exception handling. |
easynav_core/src/easynav_core/LocalizerMethodBase.cpp |
Adds localizer exception handling. |
easynav_core/src/easynav_core/ControllerMethodBase.cpp |
Removes embedded collision handling and adds exception handling. |
easynav_core/package.xml |
Updates core dependencies. |
easynav_core/include/easynav_core/SafetyReflexBase.hpp |
Defines the reflex interface. |
easynav_core/include/easynav_core/RecoveryMitigationBase.hpp |
Defines the mitigation interface. |
easynav_core/include/easynav_core/RecoveryEvaluatorBase.hpp |
Defines the evaluator interface. |
easynav_core/include/easynav_core/ObstacleProximity.hpp |
Declares the proximity API. |
easynav_core/include/easynav_core/ControllerMethodBase.hpp |
Removes collision-specific API. |
easynav_core/CMakeLists.txt |
Builds new core recovery components. |
easynav_controller/src/easynav_controller/CollisionSafetyReflex.cpp |
Implements the collision safety plugin. |
easynav_controller/package.xml |
Adds reflex dependencies. |
easynav_controller/include/easynav_controller/CollisionSafetyReflex.hpp |
Declares the collision reflex. |
easynav_controller/easynav_safety_reflexes_plugins.xml |
Registers the collision reflex. |
easynav_controller/CMakeLists.txt |
Builds and exports the reflex plugin. |
easynav_common/tests/navstate_tests.cpp |
Tests NavState grouping and ordering. |
easynav_common/include/easynav_common/types/NavState.hpp |
Adds grouped key access and sorted debugging. |
docs/recoveries_easynav.md |
Documents recovery architecture and deployment. Nit (2 votes): documentation advertises evaluator, mitigation, and AMCL plugins that are not included or registered. |
Review details
Suppressed comments (11)
easynav_core/src/easynav_core/RecoveryMitigationBase.cpp:110
- Control-owning mitigations are executed from
cycle_rt(), and theiron_cycle()implementations are allowed to callreport(). This path performs synchronous ROS logging and constructs/copies aMitigationReportbefore writing it intoNavState, all of which can allocate or block in the RT loop. Queue only RT-safe data and publish/log it from the non-RT cycle, or explicitly prohibitreport()from RT mitigations.
switch (level) {
case rcl_interfaces::msg::Log::DEBUG:
RCLCPP_DEBUG(node->get_logger(), "%s", msg.c_str());
break;
case rcl_interfaces::msg::Log::WARN:
easynav_recovery/easynav_recovery_plugins.xml:5
- This manifest registers only the dummy evaluator, while the PR description and deployment example advertise
NoPathEvaluator,ControllerStuckEvaluator,ObstacleTooCloseEvaluator, and the AMCL-specific evaluator. Those implementations/manifests are not present in this checkout, so the documented evaluator configuration cannot load the claimed catalog; either include/register it or correct the description and docs.
<library path="dummy_evaluator">
<class name="easynav_recovery/DummyEvaluator" type="easynav::DummyEvaluator" base_class_type="easynav::RecoveryEvaluatorBase">
<description>
A default "dummy" implementation for RecoveryEvaluatorBase. Always reports OK.
easynav_recovery/easynav_recovery_plugins.xml:14
- This manifest registers only
DummyMitigation, while the PR description and deployment example advertise four generic mitigations plusAmclRelocalizeMitigation. No corresponding mitigation plugin is registered here or present in this checkout, so the documentedmitigation_typesconfiguration cannot load those recovery actions; include/register them or correct the claimed scope.
<library path="dummy_mitigation">
<class name="easynav_recovery/DummyMitigation" type="easynav::DummyMitigation" base_class_type="easynav::RecoveryMitigationBase">
<description>
A default "dummy" implementation for RecoveryMitigationBase. Accepts any non-OK
diagnostic and immediately reports SUCCEEDED. Serves as an example and a reference plugin.
easynav_recovery/src/easynav_recovery/RecoveryManagerNode.cpp:378
NavState::get()returns a reference after releasingstate_mutex_, whileSafetyReflexBasecan update this diagnostic from the RT thread.statuscan therefore be read while the RT thread overwrites it, causing a data race duringcan_handle(); useget_safe<DiagnosticStatus>()into a local value here, aspublish_diagnostics()does above.
easynav_recovery/src/easynav_recovery/RecoveryManagerNode.cpp:359- The RT completion path has the same reselection problem: after a control-owning mitigation returns
SUCCEEDED, it is reset while its triggering diagnostic can still be ERROR, so the next non-RT cycle selects it again. Keep a completed mitigation excluded for that diagnostic (or otherwise hold the episode) until the evaluator reports OK.
easynav_recovery/src/easynav_recovery/RecoveryManagerNode.cpp:364 - The return value is documented as true only when the mitigation produced
cmd_vel, but this is unconditional for any active control mitigation. A plugin can returnSUCCEEDEDorRUNNINGwithout writing a command (DummyMitigationdoes), soSystemNodemay publish a stale previous command or claim a write that never happened; track/validate a fresh command or make the interface enforce that contract before returning true.
easynav_recovery/src/easynav_recovery/RecoveryManagerNode.cpp:105 - If evaluator loading fails before
on_configure()reaches the mitigation section,mitigation_typesis never declared. The destructor still callsget_parameter("mitigation_types", ...), so the failure path can throw again during destruction and terminate the test/process; guard this parameter read just as the plugin-specific reads are guarded.
easynav_recovery/src/easynav_recovery/RecoveryManagerNode.cpp:89 RecoveryManagerNodecan be destroyed without ever being configured (thenode_nametest does exactly that), but this unconditionalget_parameter()throws whenevaluator_typesis undeclared. That makes ordinary construction/destruction and early lifecycle failures terminate instead of cleaning up; guard the top-level parameter reads withhas_parameter()(and do the same for the mitigation list).
easynav_recovery/src/easynav_recovery/RecoveryManagerNode.cpp:312- This existence check reads
NavState::values_throughhas()without the state mutex whileRecoveryMitigationBase::report()may insert or updatemitigation.pending_reportfrom the RT loop. The subsequentget_safe()does not make the preceding check safe; use a thread-safe optional/snapshot lookup instead.
easynav_system/CMakeLists.txt:23 diagnostic_msgsis now used directly byGoalManager.cpp, but this CMake change only callsfind_package; the message target is absent fromtarget_link_libraries()and fromament_export_dependencies(). The build currently relies oneasynav_recovery's transitive dependency, which is fragile and can leave downstream consumers without the direct include/link dependency. Add the target and export it explicitly.
easynav_system/src/easynav_system/SystemNode.cpp:93- If any managed child fails configuration before this point (for example, a bad recovery plugin),
safety_reflex_typeshas never been declared onSystemNode. The destructor then callsget_parameter()on an undeclared parameter, which can throw during destruction and terminate the process instead of allowing the configuration failure to be reported; guard the read withhas_parameter().
- Files reviewed: 50/50 changed files
- Comments generated: 14
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+39
to
+40
| auto view = PointPerceptionsOpsView(perceptions); | ||
| view.fuse(tf_info.robot_frame); |
Comment on lines
+42
to
+46
| try { | ||
| on_start(nav_state); | ||
| } catch (const std::exception & e) { | ||
| if (auto node = get_node()) { | ||
| RCLCPP_ERROR_THROTTLE( |
Comment on lines
+110
to
+113
| auto members = nav_state.get_group_keys("diagnostics"); | ||
| if (std::find(members.begin(), members.end(), key) == members.end()) { | ||
| members.push_back(key); | ||
| nav_state.set_group("diagnostics", members); |
Comment on lines
+298
to
+303
| for (const auto & key : nav_state.get_group_keys("diagnostics")) { | ||
| if (!nav_state.has(key)) {continue;} | ||
| // Some entries (e.g. a SafetyReflexBase's) are written from the RT cycle; this runs on | ||
| // the non-RT cycle, so get_safe() (a snapshot copy) is required here, not get(). See | ||
| // NavState's own get()/get_safe() guidance. | ||
| array.status.push_back(nav_state.get_safe<diagnostic_msgs::msg::DiagnosticStatus>(key)); |
Comment on lines
+350
to
+354
| if (!active_mitigation_ || !active_mitigation_->requires_control()) { | ||
| return false; | ||
| } | ||
|
|
||
| RecoveryStatus status = active_mitigation_->internal_cycle(*nav_state); |
| <depend>easynav_maps_manager</depend> | ||
| <depend>easynav_planner</depend> | ||
| <depend>easynav_localizer</depend> | ||
| <depend>easynav_recovery</depend> |
Comment on lines
+271
to
+275
| if (status != RecoveryStatus::RUNNING) { | ||
| if (status == RecoveryStatus::FAILED) { | ||
| excluded_mitigations_[active_diagnostic_key_].insert( | ||
| active_mitigation_->get_plugin_name()); | ||
| } |
Comment on lines
+398
to
+403
| for (const auto & key : nav_state.get_group_keys("diagnostics")) { | ||
| if (!nav_state.has(key)) {continue;} | ||
| // Some "diagnostics" entries (e.g. a SafetyReflexBase's) are written from the RT cycle; | ||
| // this runs on the non-RT cycle, so get_safe() (a snapshot copy) is required here, not | ||
| // get(). See NavState's own get()/get_safe() guidance. | ||
| const auto status = nav_state.get_safe<diagnostic_msgs::msg::DiagnosticStatus>(key); |
Comment on lines
+317
to
+319
| def is_resolved_sentinel(msg: Log) -> bool: | ||
| """Check whether msg is RecoveryManagerNode's "clear your log" marker, not a report.""" | ||
| return msg.level == Log.DEBUG |
Comment on lines
+389
to
+391
| | `NoPathEvaluator`, `ControllerStuckEvaluator`, `ObstacleTooCloseEvaluator` (generic evaluators) | `src/easynav_plugins/recovery_evaluators/...` | `easynav_simple_controller`, `easynav_vff_controller`, ... | | ||
| | `SafeRetreatRecovery`, `AdvanceRecovery`, `HumanAssistanceRecovery`, `CancelMissionRecovery` (generic mitigations) | `src/easynav_plugins/recovery_mitigations/...` | ídem | | ||
| | `AmclConvergenceEvaluator` / `AmclRelocalizeMitigation` (component-specialized recovery) | `easynav_costmap_localizer`, alongside `AMCLLocalizer` | New pattern — see §5.9 | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hi,
This PR is a joint effort from @estherag and me to have a Recovery System for EasyNav. It is a Wok in Progress, so more work have to be done in this PR.
EasyNav's recovery system: a two-level architecture that detects and reacts to
navigation failures, combining Nav2's lightweight plugin mechanics with the
evaluation/mitigation separation and mission-level escalation proposed by TOMASys/SysSelf,
without requiring OWL/DL reasoning.
SafetyReflexBaseplugin checked on every RTcycle, right before
cmd_velis published, regardless of who produced it.RecoveryManagerNodethat runsRecoveryEvaluatorBasediagnosis plugins and arbitrates betweenRecoveryMitigationBaseplugins, handing off
cmd_velownership via acontrol_ownerkey when a mitigation needsto move the robot.
Full design + as-built documentation:
docs/recoveries_easynav.md.Demo
Architecture
flowchart TB subgraph proc["easynav_system process (single process, shared NavState)"] SEN[SensorsNode] --> NS[(NavState)] LOC[LocalizerNode] --> NS MAP[MapsManagerNode] --> NS PLN[PlannerNode] --> NS CTR["ControllerNode\n(active when control_owner=controller)"] --> CVEL[(candidate cmd_vel)] MIT --> CVEL subgraph rt["Level 0 - SystemNode RT cycle"] CVEL --> REFLEX["SafetyReflexBase plugins\n(CollisionSafetyReflex)"] REFLEX -->|final cmd_vel| PUB[/publish cmd_vel/] end REFLEX -.diagnostics.-> DIAG NS --> EV1[Evaluator plugin A] NS --> EV2[Evaluator plugin B] NS --> EVn[Evaluator plugin N] EV1 --> DIAG[("NavState 'diagnostics' group")] EV2 --> DIAG EVn --> DIAG DIAG --> RM["Level 1 - RecoveryManagerNode\nselection + arbitration (non-RT)"] RM --> MIT[Active mitigation plugin] RM -->|writes control_owner| NS RM -->|publishes| PUBDIAG[/diagnostics/] RM -->|publishes| PUBMIT[mitigation] MIT -.mission_cancel_requested.-> GM[GoalManager] endWhat's included
SafetyReflexBaseinterface (level 0, RT) +CollisionSafetyReflexreference plugin, replacing the old collision check that lived inControllerMethodBaseRecoveryEvaluatorBaseinterface (level 1, read-only diagnosis)RecoveryMitigationBaseinterface (level 1, acts), with priority + exclusion arbitration (no cooldown/retry table)easynav_recoverypackage:RecoveryManagerNode(lifecycle noderecovery_node), wired intoSystemNode's RT and non-RT cyclescontrol_ownerhandoff mechanism for movement mitigations to take overcmd_velNoPathEvaluator,ControllerStuckEvaluator,ObstacleTooCloseEvaluatorSafeRetreatRecovery,AdvanceRecovery,HumanAssistanceRecovery,CancelMissionRecoveryAmclConvergenceEvaluator/AmclRelocalizeMitigationco-located ineasynav_costmap_localizerGoalManager::set_error()wired to a real path via themission_cancel_requestedsignal/diagnostics(DiagnosticArray), a"mitigation"narrative topic, and TUI panels for bothRecoveryManagerNodeand every new evaluator/mitigation plugindocs/recoveries_easynav.md)Example of parameters:
These are the parameters used in the video above.