Package Summary
| Version | 0.6.0 |
| License | Apache-2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Checkout URI | https://github.com/selfpatch/ros2_medkit.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-07-07 |
| Dev Status | DEVELOPED |
| Released | RELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- mfaferek93
Authors
ros2_medkit_action_status_bridge
Generic drop-in bridge that turns terminal ROS 2 action goal states into
structured medkit faults. It watches the /<action>/_action/status
(action_msgs/msg/GoalStatusArray) topic that every action server
publishes, so it works across Nav2, MoveIt 2, ros2_control’s controller
actions, and any custom action with no per-project code.
Why this matters
For the core ROS 2 league the authoritative “the goal failed” verdict lives on
the action-result channel, not on /diagnostics or /rosout:
- Nav2
NavigateToPose->GoalStatus = ABORTED - MoveIt
MoveGroup->GoalStatus = ABORTED(withMoveItErrorCodein the result) - ros2_control controller actions -> aborted goals
Neither the diagnostic bridge nor the log bridge sees this. This bridge does, generically, by observing the goal-status topic.
What it does
- Discovers every action on the graph by scanning for
*/_action/statustopics (re-scanned on a timer to catch actions that appear later, to drop actions that vanish, e.g. a Nav2 lifecycle deactivate or a one-shot node, and to retry any fault whose delivery to the FaultManager was deferred). -
ABORTED (6)-> fault (SEVERITY_ERRORby default). -
CANCELED (5)-> fault only ifcanceled_is_fault(off by default; cancel is usually intentional). When enabled it emits a_CANCELEDcode. -
SUCCEEDED (4)->PASSEDto heal the action’s fault code (if enabled). -
fault_codeis<PREFIX>_<ACTION>_ABORTED(or_CANCELED), e.g.ACTION_NAVIGATE_TO_POSE_ABORTED.source_idis the action server’s node FQN (resolved from the status topic’s publisher, e.g./bt_navigator), so the fault associates with that node’s SOVD entity; it falls back to the action name only if no publisher is visible.
Per-action state, not per-goal
The fault is a property of the action, not of an individual goal. On every
status message the whole GoalStatusArray is scanned for the net state:
- if any goal is
ABORTED(orCANCELEDwhencanceled_is_fault), the action is failed (order of goals in the array does not matter); - otherwise, if the array has terminal goals and none are failing, the action is healthy.
A fault is raised only on the healthy -> failed transition and healed only on
failed -> healthy. This means one failed goal cannot heal an action while
another goal is still failed, and a dropped terminal message cannot leave a
fault stuck. Per-goal dedup (keyed on goal_id:status) only suppresses
duplicate log lines; it never gates the transition.
The bridge tracks the latest observed state (the desired state) separately from what the FaultManager was actually told. The transition is committed only once the report is delivered: if the FaultManager service is not discovered yet
- e.g. an action’s status topic is
transient_local(latched) and its terminal status reaches the bridge during the startup discovery window, before the report client connects - the transition stays pending and is retried, rather than the high-severity fault being silently dropped. (The latched status is delivered to a subscription only once, so without this retry the dropped report would never recover.) See Delivery latency and freeze-frame.
Scope: the terminal verdict, not the reason
This bridge delivers the generic “it aborted” event. The action-specific
reason (e.g. MoveItErrorCode.val = -26 START_STATE_IN_COLLISION, Nav2
error_code on Iron/Jazzy) lives in the action result message and is a separate
enrichment concern (a future action-result reader using runtime message
introspection / dynmsg). Per-project plugins are only needed for human-readable
labels, not to surface the fault.
Delivery latency and freeze-frame
The FaultManager captures the freeze-frame (topic snapshot, rosbag) when it receives the report, so the value of that context decays with delivery latency: a late fault is reported against state that has already moved on. The bridge therefore optimises for delivering a fault to the FaultManager as soon as physically possible.
- Happy path (service discovered): the fault is reported synchronously in the status callback, the instant the terminal status arrives. This is the normal runtime case and is already ASAP - nothing is deferred.
-
Degraded path (service not yet discovered): a fault cannot physically be
delivered before the FaultManager
report_faultservice is discovered (startup before the FaultManager is up, or a FaultManager restart). The transition is kept pending and retried on a dedicated fast retry timer (retry_period_sec, default 50 ms), decoupled from the slow discoveryrescan. So the fault lands within one short tick of the service appearing - keeping the freeze-frame as contemporaneous as the discovery floor allows.
We deliberately do not block the (single-threaded) executor waiting for the service: blocking would stall detection of other actions’ faults during an outage to shave the last few milliseconds off one fault - a bad trade when the freeze-frame is already only ~tens of ms fresher. The retry timer is armed only while a delivery is pending, so an idle bridge does no periodic work.
Known boundaries (narrow, and shrunk further by the fast retry; both are consequences of the level-triggered, net-state model, not silent loss in the common case):
File truncated at 100 lines see the full file
Changelog for package ros2_medkit_action_status_bridge
0.6.0 (2026-06-22)
- Initial release: generic action-status bridge. Watches every
/<action>/_action/statustopic and turns ABORTED goals into FaultManager faults (<PREFIX>_<ACTION>_ABORTED), heals on a non-failing terminal state. Captures the terminal action verdict that the diagnostic and log bridges cannot see (#423). - Fault state is per-ACTION, derived from the whole
GoalStatusArrayon each message: a fault is raised only on thehealthy -> failedtransition and healed only onfailed -> healthy, so it is order-independent and resilient to dropped terminal messages. Per-goal dedup now only suppresses duplicate log lines. -
canceled_is_faultemits a status-aware<PREFIX>_<ACTION>_CANCELEDcode; such a fault heals on the next non-failing terminal or when the canceled goal ages out of the retained status array. - Vanished actions (lifecycle deactivate, one-shot nodes) are pruned on rescan.
- Parameters are range-checked/normalized at load; an incompatible action status QoS is now warned about instead of silently dropping faults.
- Ships a default configuration so the bridge starts out of the box (#459)
- Fault source is fixed at first report instead of being re-attributed later: faults raised before discovery settles register the server FQN, faults are never attributed to the discovery placeholder node name, and the subscriptions and timer are reset in the destructor (#466)
- Contributors: \@bburda, \@mfaferek93
Package Dependencies
System Dependencies
Dependant Packages
| Name | Deps |
|---|---|
| ros2_medkit_gateway |
Launch files
Messages
Services
Plugins
Recent questions tagged ros2_medkit_action_status_bridge at Robotics Stack Exchange
Package Summary
| Version | 0.6.0 |
| License | Apache-2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Checkout URI | https://github.com/selfpatch/ros2_medkit.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-07-07 |
| Dev Status | DEVELOPED |
| Released | RELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- mfaferek93
Authors
ros2_medkit_action_status_bridge
Generic drop-in bridge that turns terminal ROS 2 action goal states into
structured medkit faults. It watches the /<action>/_action/status
(action_msgs/msg/GoalStatusArray) topic that every action server
publishes, so it works across Nav2, MoveIt 2, ros2_control’s controller
actions, and any custom action with no per-project code.
Why this matters
For the core ROS 2 league the authoritative “the goal failed” verdict lives on
the action-result channel, not on /diagnostics or /rosout:
- Nav2
NavigateToPose->GoalStatus = ABORTED - MoveIt
MoveGroup->GoalStatus = ABORTED(withMoveItErrorCodein the result) - ros2_control controller actions -> aborted goals
Neither the diagnostic bridge nor the log bridge sees this. This bridge does, generically, by observing the goal-status topic.
What it does
- Discovers every action on the graph by scanning for
*/_action/statustopics (re-scanned on a timer to catch actions that appear later, to drop actions that vanish, e.g. a Nav2 lifecycle deactivate or a one-shot node, and to retry any fault whose delivery to the FaultManager was deferred). -
ABORTED (6)-> fault (SEVERITY_ERRORby default). -
CANCELED (5)-> fault only ifcanceled_is_fault(off by default; cancel is usually intentional). When enabled it emits a_CANCELEDcode. -
SUCCEEDED (4)->PASSEDto heal the action’s fault code (if enabled). -
fault_codeis<PREFIX>_<ACTION>_ABORTED(or_CANCELED), e.g.ACTION_NAVIGATE_TO_POSE_ABORTED.source_idis the action server’s node FQN (resolved from the status topic’s publisher, e.g./bt_navigator), so the fault associates with that node’s SOVD entity; it falls back to the action name only if no publisher is visible.
Per-action state, not per-goal
The fault is a property of the action, not of an individual goal. On every
status message the whole GoalStatusArray is scanned for the net state:
- if any goal is
ABORTED(orCANCELEDwhencanceled_is_fault), the action is failed (order of goals in the array does not matter); - otherwise, if the array has terminal goals and none are failing, the action is healthy.
A fault is raised only on the healthy -> failed transition and healed only on
failed -> healthy. This means one failed goal cannot heal an action while
another goal is still failed, and a dropped terminal message cannot leave a
fault stuck. Per-goal dedup (keyed on goal_id:status) only suppresses
duplicate log lines; it never gates the transition.
The bridge tracks the latest observed state (the desired state) separately from what the FaultManager was actually told. The transition is committed only once the report is delivered: if the FaultManager service is not discovered yet
- e.g. an action’s status topic is
transient_local(latched) and its terminal status reaches the bridge during the startup discovery window, before the report client connects - the transition stays pending and is retried, rather than the high-severity fault being silently dropped. (The latched status is delivered to a subscription only once, so without this retry the dropped report would never recover.) See Delivery latency and freeze-frame.
Scope: the terminal verdict, not the reason
This bridge delivers the generic “it aborted” event. The action-specific
reason (e.g. MoveItErrorCode.val = -26 START_STATE_IN_COLLISION, Nav2
error_code on Iron/Jazzy) lives in the action result message and is a separate
enrichment concern (a future action-result reader using runtime message
introspection / dynmsg). Per-project plugins are only needed for human-readable
labels, not to surface the fault.
Delivery latency and freeze-frame
The FaultManager captures the freeze-frame (topic snapshot, rosbag) when it receives the report, so the value of that context decays with delivery latency: a late fault is reported against state that has already moved on. The bridge therefore optimises for delivering a fault to the FaultManager as soon as physically possible.
- Happy path (service discovered): the fault is reported synchronously in the status callback, the instant the terminal status arrives. This is the normal runtime case and is already ASAP - nothing is deferred.
-
Degraded path (service not yet discovered): a fault cannot physically be
delivered before the FaultManager
report_faultservice is discovered (startup before the FaultManager is up, or a FaultManager restart). The transition is kept pending and retried on a dedicated fast retry timer (retry_period_sec, default 50 ms), decoupled from the slow discoveryrescan. So the fault lands within one short tick of the service appearing - keeping the freeze-frame as contemporaneous as the discovery floor allows.
We deliberately do not block the (single-threaded) executor waiting for the service: blocking would stall detection of other actions’ faults during an outage to shave the last few milliseconds off one fault - a bad trade when the freeze-frame is already only ~tens of ms fresher. The retry timer is armed only while a delivery is pending, so an idle bridge does no periodic work.
Known boundaries (narrow, and shrunk further by the fast retry; both are consequences of the level-triggered, net-state model, not silent loss in the common case):
File truncated at 100 lines see the full file
Changelog for package ros2_medkit_action_status_bridge
0.6.0 (2026-06-22)
- Initial release: generic action-status bridge. Watches every
/<action>/_action/statustopic and turns ABORTED goals into FaultManager faults (<PREFIX>_<ACTION>_ABORTED), heals on a non-failing terminal state. Captures the terminal action verdict that the diagnostic and log bridges cannot see (#423). - Fault state is per-ACTION, derived from the whole
GoalStatusArrayon each message: a fault is raised only on thehealthy -> failedtransition and healed only onfailed -> healthy, so it is order-independent and resilient to dropped terminal messages. Per-goal dedup now only suppresses duplicate log lines. -
canceled_is_faultemits a status-aware<PREFIX>_<ACTION>_CANCELEDcode; such a fault heals on the next non-failing terminal or when the canceled goal ages out of the retained status array. - Vanished actions (lifecycle deactivate, one-shot nodes) are pruned on rescan.
- Parameters are range-checked/normalized at load; an incompatible action status QoS is now warned about instead of silently dropping faults.
- Ships a default configuration so the bridge starts out of the box (#459)
- Fault source is fixed at first report instead of being re-attributed later: faults raised before discovery settles register the server FQN, faults are never attributed to the discovery placeholder node name, and the subscriptions and timer are reset in the destructor (#466)
- Contributors: \@bburda, \@mfaferek93
Package Dependencies
System Dependencies
Dependant Packages
| Name | Deps |
|---|---|
| ros2_medkit_gateway |
Launch files
Messages
Services
Plugins
Recent questions tagged ros2_medkit_action_status_bridge at Robotics Stack Exchange
Package Summary
| Version | 0.6.0 |
| License | Apache-2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Checkout URI | https://github.com/selfpatch/ros2_medkit.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-07-07 |
| Dev Status | DEVELOPED |
| Released | RELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- mfaferek93
Authors
ros2_medkit_action_status_bridge
Generic drop-in bridge that turns terminal ROS 2 action goal states into
structured medkit faults. It watches the /<action>/_action/status
(action_msgs/msg/GoalStatusArray) topic that every action server
publishes, so it works across Nav2, MoveIt 2, ros2_control’s controller
actions, and any custom action with no per-project code.
Why this matters
For the core ROS 2 league the authoritative “the goal failed” verdict lives on
the action-result channel, not on /diagnostics or /rosout:
- Nav2
NavigateToPose->GoalStatus = ABORTED - MoveIt
MoveGroup->GoalStatus = ABORTED(withMoveItErrorCodein the result) - ros2_control controller actions -> aborted goals
Neither the diagnostic bridge nor the log bridge sees this. This bridge does, generically, by observing the goal-status topic.
What it does
- Discovers every action on the graph by scanning for
*/_action/statustopics (re-scanned on a timer to catch actions that appear later, to drop actions that vanish, e.g. a Nav2 lifecycle deactivate or a one-shot node, and to retry any fault whose delivery to the FaultManager was deferred). -
ABORTED (6)-> fault (SEVERITY_ERRORby default). -
CANCELED (5)-> fault only ifcanceled_is_fault(off by default; cancel is usually intentional). When enabled it emits a_CANCELEDcode. -
SUCCEEDED (4)->PASSEDto heal the action’s fault code (if enabled). -
fault_codeis<PREFIX>_<ACTION>_ABORTED(or_CANCELED), e.g.ACTION_NAVIGATE_TO_POSE_ABORTED.source_idis the action server’s node FQN (resolved from the status topic’s publisher, e.g./bt_navigator), so the fault associates with that node’s SOVD entity; it falls back to the action name only if no publisher is visible.
Per-action state, not per-goal
The fault is a property of the action, not of an individual goal. On every
status message the whole GoalStatusArray is scanned for the net state:
- if any goal is
ABORTED(orCANCELEDwhencanceled_is_fault), the action is failed (order of goals in the array does not matter); - otherwise, if the array has terminal goals and none are failing, the action is healthy.
A fault is raised only on the healthy -> failed transition and healed only on
failed -> healthy. This means one failed goal cannot heal an action while
another goal is still failed, and a dropped terminal message cannot leave a
fault stuck. Per-goal dedup (keyed on goal_id:status) only suppresses
duplicate log lines; it never gates the transition.
The bridge tracks the latest observed state (the desired state) separately from what the FaultManager was actually told. The transition is committed only once the report is delivered: if the FaultManager service is not discovered yet
- e.g. an action’s status topic is
transient_local(latched) and its terminal status reaches the bridge during the startup discovery window, before the report client connects - the transition stays pending and is retried, rather than the high-severity fault being silently dropped. (The latched status is delivered to a subscription only once, so without this retry the dropped report would never recover.) See Delivery latency and freeze-frame.
Scope: the terminal verdict, not the reason
This bridge delivers the generic “it aborted” event. The action-specific
reason (e.g. MoveItErrorCode.val = -26 START_STATE_IN_COLLISION, Nav2
error_code on Iron/Jazzy) lives in the action result message and is a separate
enrichment concern (a future action-result reader using runtime message
introspection / dynmsg). Per-project plugins are only needed for human-readable
labels, not to surface the fault.
Delivery latency and freeze-frame
The FaultManager captures the freeze-frame (topic snapshot, rosbag) when it receives the report, so the value of that context decays with delivery latency: a late fault is reported against state that has already moved on. The bridge therefore optimises for delivering a fault to the FaultManager as soon as physically possible.
- Happy path (service discovered): the fault is reported synchronously in the status callback, the instant the terminal status arrives. This is the normal runtime case and is already ASAP - nothing is deferred.
-
Degraded path (service not yet discovered): a fault cannot physically be
delivered before the FaultManager
report_faultservice is discovered (startup before the FaultManager is up, or a FaultManager restart). The transition is kept pending and retried on a dedicated fast retry timer (retry_period_sec, default 50 ms), decoupled from the slow discoveryrescan. So the fault lands within one short tick of the service appearing - keeping the freeze-frame as contemporaneous as the discovery floor allows.
We deliberately do not block the (single-threaded) executor waiting for the service: blocking would stall detection of other actions’ faults during an outage to shave the last few milliseconds off one fault - a bad trade when the freeze-frame is already only ~tens of ms fresher. The retry timer is armed only while a delivery is pending, so an idle bridge does no periodic work.
Known boundaries (narrow, and shrunk further by the fast retry; both are consequences of the level-triggered, net-state model, not silent loss in the common case):
File truncated at 100 lines see the full file
Changelog for package ros2_medkit_action_status_bridge
0.6.0 (2026-06-22)
- Initial release: generic action-status bridge. Watches every
/<action>/_action/statustopic and turns ABORTED goals into FaultManager faults (<PREFIX>_<ACTION>_ABORTED), heals on a non-failing terminal state. Captures the terminal action verdict that the diagnostic and log bridges cannot see (#423). - Fault state is per-ACTION, derived from the whole
GoalStatusArrayon each message: a fault is raised only on thehealthy -> failedtransition and healed only onfailed -> healthy, so it is order-independent and resilient to dropped terminal messages. Per-goal dedup now only suppresses duplicate log lines. -
canceled_is_faultemits a status-aware<PREFIX>_<ACTION>_CANCELEDcode; such a fault heals on the next non-failing terminal or when the canceled goal ages out of the retained status array. - Vanished actions (lifecycle deactivate, one-shot nodes) are pruned on rescan.
- Parameters are range-checked/normalized at load; an incompatible action status QoS is now warned about instead of silently dropping faults.
- Ships a default configuration so the bridge starts out of the box (#459)
- Fault source is fixed at first report instead of being re-attributed later: faults raised before discovery settles register the server FQN, faults are never attributed to the discovery placeholder node name, and the subscriptions and timer are reset in the destructor (#466)
- Contributors: \@bburda, \@mfaferek93
Package Dependencies
System Dependencies
Dependant Packages
| Name | Deps |
|---|---|
| ros2_medkit_gateway |
Launch files
Messages
Services
Plugins
Recent questions tagged ros2_medkit_action_status_bridge at Robotics Stack Exchange
Package Summary
| Version | 0.6.0 |
| License | Apache-2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Checkout URI | https://github.com/selfpatch/ros2_medkit.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-07-07 |
| Dev Status | DEVELOPED |
| Released | RELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- mfaferek93
Authors
ros2_medkit_action_status_bridge
Generic drop-in bridge that turns terminal ROS 2 action goal states into
structured medkit faults. It watches the /<action>/_action/status
(action_msgs/msg/GoalStatusArray) topic that every action server
publishes, so it works across Nav2, MoveIt 2, ros2_control’s controller
actions, and any custom action with no per-project code.
Why this matters
For the core ROS 2 league the authoritative “the goal failed” verdict lives on
the action-result channel, not on /diagnostics or /rosout:
- Nav2
NavigateToPose->GoalStatus = ABORTED - MoveIt
MoveGroup->GoalStatus = ABORTED(withMoveItErrorCodein the result) - ros2_control controller actions -> aborted goals
Neither the diagnostic bridge nor the log bridge sees this. This bridge does, generically, by observing the goal-status topic.
What it does
- Discovers every action on the graph by scanning for
*/_action/statustopics (re-scanned on a timer to catch actions that appear later, to drop actions that vanish, e.g. a Nav2 lifecycle deactivate or a one-shot node, and to retry any fault whose delivery to the FaultManager was deferred). -
ABORTED (6)-> fault (SEVERITY_ERRORby default). -
CANCELED (5)-> fault only ifcanceled_is_fault(off by default; cancel is usually intentional). When enabled it emits a_CANCELEDcode. -
SUCCEEDED (4)->PASSEDto heal the action’s fault code (if enabled). -
fault_codeis<PREFIX>_<ACTION>_ABORTED(or_CANCELED), e.g.ACTION_NAVIGATE_TO_POSE_ABORTED.source_idis the action server’s node FQN (resolved from the status topic’s publisher, e.g./bt_navigator), so the fault associates with that node’s SOVD entity; it falls back to the action name only if no publisher is visible.
Per-action state, not per-goal
The fault is a property of the action, not of an individual goal. On every
status message the whole GoalStatusArray is scanned for the net state:
- if any goal is
ABORTED(orCANCELEDwhencanceled_is_fault), the action is failed (order of goals in the array does not matter); - otherwise, if the array has terminal goals and none are failing, the action is healthy.
A fault is raised only on the healthy -> failed transition and healed only on
failed -> healthy. This means one failed goal cannot heal an action while
another goal is still failed, and a dropped terminal message cannot leave a
fault stuck. Per-goal dedup (keyed on goal_id:status) only suppresses
duplicate log lines; it never gates the transition.
The bridge tracks the latest observed state (the desired state) separately from what the FaultManager was actually told. The transition is committed only once the report is delivered: if the FaultManager service is not discovered yet
- e.g. an action’s status topic is
transient_local(latched) and its terminal status reaches the bridge during the startup discovery window, before the report client connects - the transition stays pending and is retried, rather than the high-severity fault being silently dropped. (The latched status is delivered to a subscription only once, so without this retry the dropped report would never recover.) See Delivery latency and freeze-frame.
Scope: the terminal verdict, not the reason
This bridge delivers the generic “it aborted” event. The action-specific
reason (e.g. MoveItErrorCode.val = -26 START_STATE_IN_COLLISION, Nav2
error_code on Iron/Jazzy) lives in the action result message and is a separate
enrichment concern (a future action-result reader using runtime message
introspection / dynmsg). Per-project plugins are only needed for human-readable
labels, not to surface the fault.
Delivery latency and freeze-frame
The FaultManager captures the freeze-frame (topic snapshot, rosbag) when it receives the report, so the value of that context decays with delivery latency: a late fault is reported against state that has already moved on. The bridge therefore optimises for delivering a fault to the FaultManager as soon as physically possible.
- Happy path (service discovered): the fault is reported synchronously in the status callback, the instant the terminal status arrives. This is the normal runtime case and is already ASAP - nothing is deferred.
-
Degraded path (service not yet discovered): a fault cannot physically be
delivered before the FaultManager
report_faultservice is discovered (startup before the FaultManager is up, or a FaultManager restart). The transition is kept pending and retried on a dedicated fast retry timer (retry_period_sec, default 50 ms), decoupled from the slow discoveryrescan. So the fault lands within one short tick of the service appearing - keeping the freeze-frame as contemporaneous as the discovery floor allows.
We deliberately do not block the (single-threaded) executor waiting for the service: blocking would stall detection of other actions’ faults during an outage to shave the last few milliseconds off one fault - a bad trade when the freeze-frame is already only ~tens of ms fresher. The retry timer is armed only while a delivery is pending, so an idle bridge does no periodic work.
Known boundaries (narrow, and shrunk further by the fast retry; both are consequences of the level-triggered, net-state model, not silent loss in the common case):
File truncated at 100 lines see the full file
Changelog for package ros2_medkit_action_status_bridge
0.6.0 (2026-06-22)
- Initial release: generic action-status bridge. Watches every
/<action>/_action/statustopic and turns ABORTED goals into FaultManager faults (<PREFIX>_<ACTION>_ABORTED), heals on a non-failing terminal state. Captures the terminal action verdict that the diagnostic and log bridges cannot see (#423). - Fault state is per-ACTION, derived from the whole
GoalStatusArrayon each message: a fault is raised only on thehealthy -> failedtransition and healed only onfailed -> healthy, so it is order-independent and resilient to dropped terminal messages. Per-goal dedup now only suppresses duplicate log lines. -
canceled_is_faultemits a status-aware<PREFIX>_<ACTION>_CANCELEDcode; such a fault heals on the next non-failing terminal or when the canceled goal ages out of the retained status array. - Vanished actions (lifecycle deactivate, one-shot nodes) are pruned on rescan.
- Parameters are range-checked/normalized at load; an incompatible action status QoS is now warned about instead of silently dropping faults.
- Ships a default configuration so the bridge starts out of the box (#459)
- Fault source is fixed at first report instead of being re-attributed later: faults raised before discovery settles register the server FQN, faults are never attributed to the discovery placeholder node name, and the subscriptions and timer are reset in the destructor (#466)
- Contributors: \@bburda, \@mfaferek93
Package Dependencies
System Dependencies
Dependant Packages
| Name | Deps |
|---|---|
| ros2_medkit_gateway |
Launch files
Messages
Services
Plugins
Recent questions tagged ros2_medkit_action_status_bridge at Robotics Stack Exchange
Package Summary
| Version | 0.6.0 |
| License | Apache-2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Checkout URI | https://github.com/selfpatch/ros2_medkit.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-07-07 |
| Dev Status | DEVELOPED |
| Released | RELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- mfaferek93
Authors
ros2_medkit_action_status_bridge
Generic drop-in bridge that turns terminal ROS 2 action goal states into
structured medkit faults. It watches the /<action>/_action/status
(action_msgs/msg/GoalStatusArray) topic that every action server
publishes, so it works across Nav2, MoveIt 2, ros2_control’s controller
actions, and any custom action with no per-project code.
Why this matters
For the core ROS 2 league the authoritative “the goal failed” verdict lives on
the action-result channel, not on /diagnostics or /rosout:
- Nav2
NavigateToPose->GoalStatus = ABORTED - MoveIt
MoveGroup->GoalStatus = ABORTED(withMoveItErrorCodein the result) - ros2_control controller actions -> aborted goals
Neither the diagnostic bridge nor the log bridge sees this. This bridge does, generically, by observing the goal-status topic.
What it does
- Discovers every action on the graph by scanning for
*/_action/statustopics (re-scanned on a timer to catch actions that appear later, to drop actions that vanish, e.g. a Nav2 lifecycle deactivate or a one-shot node, and to retry any fault whose delivery to the FaultManager was deferred). -
ABORTED (6)-> fault (SEVERITY_ERRORby default). -
CANCELED (5)-> fault only ifcanceled_is_fault(off by default; cancel is usually intentional). When enabled it emits a_CANCELEDcode. -
SUCCEEDED (4)->PASSEDto heal the action’s fault code (if enabled). -
fault_codeis<PREFIX>_<ACTION>_ABORTED(or_CANCELED), e.g.ACTION_NAVIGATE_TO_POSE_ABORTED.source_idis the action server’s node FQN (resolved from the status topic’s publisher, e.g./bt_navigator), so the fault associates with that node’s SOVD entity; it falls back to the action name only if no publisher is visible.
Per-action state, not per-goal
The fault is a property of the action, not of an individual goal. On every
status message the whole GoalStatusArray is scanned for the net state:
- if any goal is
ABORTED(orCANCELEDwhencanceled_is_fault), the action is failed (order of goals in the array does not matter); - otherwise, if the array has terminal goals and none are failing, the action is healthy.
A fault is raised only on the healthy -> failed transition and healed only on
failed -> healthy. This means one failed goal cannot heal an action while
another goal is still failed, and a dropped terminal message cannot leave a
fault stuck. Per-goal dedup (keyed on goal_id:status) only suppresses
duplicate log lines; it never gates the transition.
The bridge tracks the latest observed state (the desired state) separately from what the FaultManager was actually told. The transition is committed only once the report is delivered: if the FaultManager service is not discovered yet
- e.g. an action’s status topic is
transient_local(latched) and its terminal status reaches the bridge during the startup discovery window, before the report client connects - the transition stays pending and is retried, rather than the high-severity fault being silently dropped. (The latched status is delivered to a subscription only once, so without this retry the dropped report would never recover.) See Delivery latency and freeze-frame.
Scope: the terminal verdict, not the reason
This bridge delivers the generic “it aborted” event. The action-specific
reason (e.g. MoveItErrorCode.val = -26 START_STATE_IN_COLLISION, Nav2
error_code on Iron/Jazzy) lives in the action result message and is a separate
enrichment concern (a future action-result reader using runtime message
introspection / dynmsg). Per-project plugins are only needed for human-readable
labels, not to surface the fault.
Delivery latency and freeze-frame
The FaultManager captures the freeze-frame (topic snapshot, rosbag) when it receives the report, so the value of that context decays with delivery latency: a late fault is reported against state that has already moved on. The bridge therefore optimises for delivering a fault to the FaultManager as soon as physically possible.
- Happy path (service discovered): the fault is reported synchronously in the status callback, the instant the terminal status arrives. This is the normal runtime case and is already ASAP - nothing is deferred.
-
Degraded path (service not yet discovered): a fault cannot physically be
delivered before the FaultManager
report_faultservice is discovered (startup before the FaultManager is up, or a FaultManager restart). The transition is kept pending and retried on a dedicated fast retry timer (retry_period_sec, default 50 ms), decoupled from the slow discoveryrescan. So the fault lands within one short tick of the service appearing - keeping the freeze-frame as contemporaneous as the discovery floor allows.
We deliberately do not block the (single-threaded) executor waiting for the service: blocking would stall detection of other actions’ faults during an outage to shave the last few milliseconds off one fault - a bad trade when the freeze-frame is already only ~tens of ms fresher. The retry timer is armed only while a delivery is pending, so an idle bridge does no periodic work.
Known boundaries (narrow, and shrunk further by the fast retry; both are consequences of the level-triggered, net-state model, not silent loss in the common case):
File truncated at 100 lines see the full file
Changelog for package ros2_medkit_action_status_bridge
0.6.0 (2026-06-22)
- Initial release: generic action-status bridge. Watches every
/<action>/_action/statustopic and turns ABORTED goals into FaultManager faults (<PREFIX>_<ACTION>_ABORTED), heals on a non-failing terminal state. Captures the terminal action verdict that the diagnostic and log bridges cannot see (#423). - Fault state is per-ACTION, derived from the whole
GoalStatusArrayon each message: a fault is raised only on thehealthy -> failedtransition and healed only onfailed -> healthy, so it is order-independent and resilient to dropped terminal messages. Per-goal dedup now only suppresses duplicate log lines. -
canceled_is_faultemits a status-aware<PREFIX>_<ACTION>_CANCELEDcode; such a fault heals on the next non-failing terminal or when the canceled goal ages out of the retained status array. - Vanished actions (lifecycle deactivate, one-shot nodes) are pruned on rescan.
- Parameters are range-checked/normalized at load; an incompatible action status QoS is now warned about instead of silently dropping faults.
- Ships a default configuration so the bridge starts out of the box (#459)
- Fault source is fixed at first report instead of being re-attributed later: faults raised before discovery settles register the server FQN, faults are never attributed to the discovery placeholder node name, and the subscriptions and timer are reset in the destructor (#466)
- Contributors: \@bburda, \@mfaferek93
Package Dependencies
System Dependencies
Dependant Packages
| Name | Deps |
|---|---|
| ros2_medkit_gateway |
Launch files
Messages
Services
Plugins
Recent questions tagged ros2_medkit_action_status_bridge at Robotics Stack Exchange
Package Summary
| Version | 0.6.0 |
| License | Apache-2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Checkout URI | https://github.com/selfpatch/ros2_medkit.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-07-07 |
| Dev Status | DEVELOPED |
| Released | RELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- mfaferek93
Authors
ros2_medkit_action_status_bridge
Generic drop-in bridge that turns terminal ROS 2 action goal states into
structured medkit faults. It watches the /<action>/_action/status
(action_msgs/msg/GoalStatusArray) topic that every action server
publishes, so it works across Nav2, MoveIt 2, ros2_control’s controller
actions, and any custom action with no per-project code.
Why this matters
For the core ROS 2 league the authoritative “the goal failed” verdict lives on
the action-result channel, not on /diagnostics or /rosout:
- Nav2
NavigateToPose->GoalStatus = ABORTED - MoveIt
MoveGroup->GoalStatus = ABORTED(withMoveItErrorCodein the result) - ros2_control controller actions -> aborted goals
Neither the diagnostic bridge nor the log bridge sees this. This bridge does, generically, by observing the goal-status topic.
What it does
- Discovers every action on the graph by scanning for
*/_action/statustopics (re-scanned on a timer to catch actions that appear later, to drop actions that vanish, e.g. a Nav2 lifecycle deactivate or a one-shot node, and to retry any fault whose delivery to the FaultManager was deferred). -
ABORTED (6)-> fault (SEVERITY_ERRORby default). -
CANCELED (5)-> fault only ifcanceled_is_fault(off by default; cancel is usually intentional). When enabled it emits a_CANCELEDcode. -
SUCCEEDED (4)->PASSEDto heal the action’s fault code (if enabled). -
fault_codeis<PREFIX>_<ACTION>_ABORTED(or_CANCELED), e.g.ACTION_NAVIGATE_TO_POSE_ABORTED.source_idis the action server’s node FQN (resolved from the status topic’s publisher, e.g./bt_navigator), so the fault associates with that node’s SOVD entity; it falls back to the action name only if no publisher is visible.
Per-action state, not per-goal
The fault is a property of the action, not of an individual goal. On every
status message the whole GoalStatusArray is scanned for the net state:
- if any goal is
ABORTED(orCANCELEDwhencanceled_is_fault), the action is failed (order of goals in the array does not matter); - otherwise, if the array has terminal goals and none are failing, the action is healthy.
A fault is raised only on the healthy -> failed transition and healed only on
failed -> healthy. This means one failed goal cannot heal an action while
another goal is still failed, and a dropped terminal message cannot leave a
fault stuck. Per-goal dedup (keyed on goal_id:status) only suppresses
duplicate log lines; it never gates the transition.
The bridge tracks the latest observed state (the desired state) separately from what the FaultManager was actually told. The transition is committed only once the report is delivered: if the FaultManager service is not discovered yet
- e.g. an action’s status topic is
transient_local(latched) and its terminal status reaches the bridge during the startup discovery window, before the report client connects - the transition stays pending and is retried, rather than the high-severity fault being silently dropped. (The latched status is delivered to a subscription only once, so without this retry the dropped report would never recover.) See Delivery latency and freeze-frame.
Scope: the terminal verdict, not the reason
This bridge delivers the generic “it aborted” event. The action-specific
reason (e.g. MoveItErrorCode.val = -26 START_STATE_IN_COLLISION, Nav2
error_code on Iron/Jazzy) lives in the action result message and is a separate
enrichment concern (a future action-result reader using runtime message
introspection / dynmsg). Per-project plugins are only needed for human-readable
labels, not to surface the fault.
Delivery latency and freeze-frame
The FaultManager captures the freeze-frame (topic snapshot, rosbag) when it receives the report, so the value of that context decays with delivery latency: a late fault is reported against state that has already moved on. The bridge therefore optimises for delivering a fault to the FaultManager as soon as physically possible.
- Happy path (service discovered): the fault is reported synchronously in the status callback, the instant the terminal status arrives. This is the normal runtime case and is already ASAP - nothing is deferred.
-
Degraded path (service not yet discovered): a fault cannot physically be
delivered before the FaultManager
report_faultservice is discovered (startup before the FaultManager is up, or a FaultManager restart). The transition is kept pending and retried on a dedicated fast retry timer (retry_period_sec, default 50 ms), decoupled from the slow discoveryrescan. So the fault lands within one short tick of the service appearing - keeping the freeze-frame as contemporaneous as the discovery floor allows.
We deliberately do not block the (single-threaded) executor waiting for the service: blocking would stall detection of other actions’ faults during an outage to shave the last few milliseconds off one fault - a bad trade when the freeze-frame is already only ~tens of ms fresher. The retry timer is armed only while a delivery is pending, so an idle bridge does no periodic work.
Known boundaries (narrow, and shrunk further by the fast retry; both are consequences of the level-triggered, net-state model, not silent loss in the common case):
File truncated at 100 lines see the full file
Changelog for package ros2_medkit_action_status_bridge
0.6.0 (2026-06-22)
- Initial release: generic action-status bridge. Watches every
/<action>/_action/statustopic and turns ABORTED goals into FaultManager faults (<PREFIX>_<ACTION>_ABORTED), heals on a non-failing terminal state. Captures the terminal action verdict that the diagnostic and log bridges cannot see (#423). - Fault state is per-ACTION, derived from the whole
GoalStatusArrayon each message: a fault is raised only on thehealthy -> failedtransition and healed only onfailed -> healthy, so it is order-independent and resilient to dropped terminal messages. Per-goal dedup now only suppresses duplicate log lines. -
canceled_is_faultemits a status-aware<PREFIX>_<ACTION>_CANCELEDcode; such a fault heals on the next non-failing terminal or when the canceled goal ages out of the retained status array. - Vanished actions (lifecycle deactivate, one-shot nodes) are pruned on rescan.
- Parameters are range-checked/normalized at load; an incompatible action status QoS is now warned about instead of silently dropping faults.
- Ships a default configuration so the bridge starts out of the box (#459)
- Fault source is fixed at first report instead of being re-attributed later: faults raised before discovery settles register the server FQN, faults are never attributed to the discovery placeholder node name, and the subscriptions and timer are reset in the destructor (#466)
- Contributors: \@bburda, \@mfaferek93
Package Dependencies
System Dependencies
Dependant Packages
| Name | Deps |
|---|---|
| ros2_medkit_gateway |
Launch files
Messages
Services
Plugins
Recent questions tagged ros2_medkit_action_status_bridge at Robotics Stack Exchange
Package Summary
| Version | 0.6.0 |
| License | Apache-2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Checkout URI | https://github.com/selfpatch/ros2_medkit.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-07-07 |
| Dev Status | DEVELOPED |
| Released | RELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- mfaferek93
Authors
ros2_medkit_action_status_bridge
Generic drop-in bridge that turns terminal ROS 2 action goal states into
structured medkit faults. It watches the /<action>/_action/status
(action_msgs/msg/GoalStatusArray) topic that every action server
publishes, so it works across Nav2, MoveIt 2, ros2_control’s controller
actions, and any custom action with no per-project code.
Why this matters
For the core ROS 2 league the authoritative “the goal failed” verdict lives on
the action-result channel, not on /diagnostics or /rosout:
- Nav2
NavigateToPose->GoalStatus = ABORTED - MoveIt
MoveGroup->GoalStatus = ABORTED(withMoveItErrorCodein the result) - ros2_control controller actions -> aborted goals
Neither the diagnostic bridge nor the log bridge sees this. This bridge does, generically, by observing the goal-status topic.
What it does
- Discovers every action on the graph by scanning for
*/_action/statustopics (re-scanned on a timer to catch actions that appear later, to drop actions that vanish, e.g. a Nav2 lifecycle deactivate or a one-shot node, and to retry any fault whose delivery to the FaultManager was deferred). -
ABORTED (6)-> fault (SEVERITY_ERRORby default). -
CANCELED (5)-> fault only ifcanceled_is_fault(off by default; cancel is usually intentional). When enabled it emits a_CANCELEDcode. -
SUCCEEDED (4)->PASSEDto heal the action’s fault code (if enabled). -
fault_codeis<PREFIX>_<ACTION>_ABORTED(or_CANCELED), e.g.ACTION_NAVIGATE_TO_POSE_ABORTED.source_idis the action server’s node FQN (resolved from the status topic’s publisher, e.g./bt_navigator), so the fault associates with that node’s SOVD entity; it falls back to the action name only if no publisher is visible.
Per-action state, not per-goal
The fault is a property of the action, not of an individual goal. On every
status message the whole GoalStatusArray is scanned for the net state:
- if any goal is
ABORTED(orCANCELEDwhencanceled_is_fault), the action is failed (order of goals in the array does not matter); - otherwise, if the array has terminal goals and none are failing, the action is healthy.
A fault is raised only on the healthy -> failed transition and healed only on
failed -> healthy. This means one failed goal cannot heal an action while
another goal is still failed, and a dropped terminal message cannot leave a
fault stuck. Per-goal dedup (keyed on goal_id:status) only suppresses
duplicate log lines; it never gates the transition.
The bridge tracks the latest observed state (the desired state) separately from what the FaultManager was actually told. The transition is committed only once the report is delivered: if the FaultManager service is not discovered yet
- e.g. an action’s status topic is
transient_local(latched) and its terminal status reaches the bridge during the startup discovery window, before the report client connects - the transition stays pending and is retried, rather than the high-severity fault being silently dropped. (The latched status is delivered to a subscription only once, so without this retry the dropped report would never recover.) See Delivery latency and freeze-frame.
Scope: the terminal verdict, not the reason
This bridge delivers the generic “it aborted” event. The action-specific
reason (e.g. MoveItErrorCode.val = -26 START_STATE_IN_COLLISION, Nav2
error_code on Iron/Jazzy) lives in the action result message and is a separate
enrichment concern (a future action-result reader using runtime message
introspection / dynmsg). Per-project plugins are only needed for human-readable
labels, not to surface the fault.
Delivery latency and freeze-frame
The FaultManager captures the freeze-frame (topic snapshot, rosbag) when it receives the report, so the value of that context decays with delivery latency: a late fault is reported against state that has already moved on. The bridge therefore optimises for delivering a fault to the FaultManager as soon as physically possible.
- Happy path (service discovered): the fault is reported synchronously in the status callback, the instant the terminal status arrives. This is the normal runtime case and is already ASAP - nothing is deferred.
-
Degraded path (service not yet discovered): a fault cannot physically be
delivered before the FaultManager
report_faultservice is discovered (startup before the FaultManager is up, or a FaultManager restart). The transition is kept pending and retried on a dedicated fast retry timer (retry_period_sec, default 50 ms), decoupled from the slow discoveryrescan. So the fault lands within one short tick of the service appearing - keeping the freeze-frame as contemporaneous as the discovery floor allows.
We deliberately do not block the (single-threaded) executor waiting for the service: blocking would stall detection of other actions’ faults during an outage to shave the last few milliseconds off one fault - a bad trade when the freeze-frame is already only ~tens of ms fresher. The retry timer is armed only while a delivery is pending, so an idle bridge does no periodic work.
Known boundaries (narrow, and shrunk further by the fast retry; both are consequences of the level-triggered, net-state model, not silent loss in the common case):
File truncated at 100 lines see the full file
Changelog for package ros2_medkit_action_status_bridge
0.6.0 (2026-06-22)
- Initial release: generic action-status bridge. Watches every
/<action>/_action/statustopic and turns ABORTED goals into FaultManager faults (<PREFIX>_<ACTION>_ABORTED), heals on a non-failing terminal state. Captures the terminal action verdict that the diagnostic and log bridges cannot see (#423). - Fault state is per-ACTION, derived from the whole
GoalStatusArrayon each message: a fault is raised only on thehealthy -> failedtransition and healed only onfailed -> healthy, so it is order-independent and resilient to dropped terminal messages. Per-goal dedup now only suppresses duplicate log lines. -
canceled_is_faultemits a status-aware<PREFIX>_<ACTION>_CANCELEDcode; such a fault heals on the next non-failing terminal or when the canceled goal ages out of the retained status array. - Vanished actions (lifecycle deactivate, one-shot nodes) are pruned on rescan.
- Parameters are range-checked/normalized at load; an incompatible action status QoS is now warned about instead of silently dropping faults.
- Ships a default configuration so the bridge starts out of the box (#459)
- Fault source is fixed at first report instead of being re-attributed later: faults raised before discovery settles register the server FQN, faults are never attributed to the discovery placeholder node name, and the subscriptions and timer are reset in the destructor (#466)
- Contributors: \@bburda, \@mfaferek93
Package Dependencies
System Dependencies
Dependant Packages
| Name | Deps |
|---|---|
| ros2_medkit_gateway |
Launch files
Messages
Services
Plugins
Recent questions tagged ros2_medkit_action_status_bridge at Robotics Stack Exchange
Package Summary
| Version | 0.6.0 |
| License | Apache-2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Checkout URI | https://github.com/selfpatch/ros2_medkit.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-07-07 |
| Dev Status | DEVELOPED |
| Released | RELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- mfaferek93
Authors
ros2_medkit_action_status_bridge
Generic drop-in bridge that turns terminal ROS 2 action goal states into
structured medkit faults. It watches the /<action>/_action/status
(action_msgs/msg/GoalStatusArray) topic that every action server
publishes, so it works across Nav2, MoveIt 2, ros2_control’s controller
actions, and any custom action with no per-project code.
Why this matters
For the core ROS 2 league the authoritative “the goal failed” verdict lives on
the action-result channel, not on /diagnostics or /rosout:
- Nav2
NavigateToPose->GoalStatus = ABORTED - MoveIt
MoveGroup->GoalStatus = ABORTED(withMoveItErrorCodein the result) - ros2_control controller actions -> aborted goals
Neither the diagnostic bridge nor the log bridge sees this. This bridge does, generically, by observing the goal-status topic.
What it does
- Discovers every action on the graph by scanning for
*/_action/statustopics (re-scanned on a timer to catch actions that appear later, to drop actions that vanish, e.g. a Nav2 lifecycle deactivate or a one-shot node, and to retry any fault whose delivery to the FaultManager was deferred). -
ABORTED (6)-> fault (SEVERITY_ERRORby default). -
CANCELED (5)-> fault only ifcanceled_is_fault(off by default; cancel is usually intentional). When enabled it emits a_CANCELEDcode. -
SUCCEEDED (4)->PASSEDto heal the action’s fault code (if enabled). -
fault_codeis<PREFIX>_<ACTION>_ABORTED(or_CANCELED), e.g.ACTION_NAVIGATE_TO_POSE_ABORTED.source_idis the action server’s node FQN (resolved from the status topic’s publisher, e.g./bt_navigator), so the fault associates with that node’s SOVD entity; it falls back to the action name only if no publisher is visible.
Per-action state, not per-goal
The fault is a property of the action, not of an individual goal. On every
status message the whole GoalStatusArray is scanned for the net state:
- if any goal is
ABORTED(orCANCELEDwhencanceled_is_fault), the action is failed (order of goals in the array does not matter); - otherwise, if the array has terminal goals and none are failing, the action is healthy.
A fault is raised only on the healthy -> failed transition and healed only on
failed -> healthy. This means one failed goal cannot heal an action while
another goal is still failed, and a dropped terminal message cannot leave a
fault stuck. Per-goal dedup (keyed on goal_id:status) only suppresses
duplicate log lines; it never gates the transition.
The bridge tracks the latest observed state (the desired state) separately from what the FaultManager was actually told. The transition is committed only once the report is delivered: if the FaultManager service is not discovered yet
- e.g. an action’s status topic is
transient_local(latched) and its terminal status reaches the bridge during the startup discovery window, before the report client connects - the transition stays pending and is retried, rather than the high-severity fault being silently dropped. (The latched status is delivered to a subscription only once, so without this retry the dropped report would never recover.) See Delivery latency and freeze-frame.
Scope: the terminal verdict, not the reason
This bridge delivers the generic “it aborted” event. The action-specific
reason (e.g. MoveItErrorCode.val = -26 START_STATE_IN_COLLISION, Nav2
error_code on Iron/Jazzy) lives in the action result message and is a separate
enrichment concern (a future action-result reader using runtime message
introspection / dynmsg). Per-project plugins are only needed for human-readable
labels, not to surface the fault.
Delivery latency and freeze-frame
The FaultManager captures the freeze-frame (topic snapshot, rosbag) when it receives the report, so the value of that context decays with delivery latency: a late fault is reported against state that has already moved on. The bridge therefore optimises for delivering a fault to the FaultManager as soon as physically possible.
- Happy path (service discovered): the fault is reported synchronously in the status callback, the instant the terminal status arrives. This is the normal runtime case and is already ASAP - nothing is deferred.
-
Degraded path (service not yet discovered): a fault cannot physically be
delivered before the FaultManager
report_faultservice is discovered (startup before the FaultManager is up, or a FaultManager restart). The transition is kept pending and retried on a dedicated fast retry timer (retry_period_sec, default 50 ms), decoupled from the slow discoveryrescan. So the fault lands within one short tick of the service appearing - keeping the freeze-frame as contemporaneous as the discovery floor allows.
We deliberately do not block the (single-threaded) executor waiting for the service: blocking would stall detection of other actions’ faults during an outage to shave the last few milliseconds off one fault - a bad trade when the freeze-frame is already only ~tens of ms fresher. The retry timer is armed only while a delivery is pending, so an idle bridge does no periodic work.
Known boundaries (narrow, and shrunk further by the fast retry; both are consequences of the level-triggered, net-state model, not silent loss in the common case):
File truncated at 100 lines see the full file
Changelog for package ros2_medkit_action_status_bridge
0.6.0 (2026-06-22)
- Initial release: generic action-status bridge. Watches every
/<action>/_action/statustopic and turns ABORTED goals into FaultManager faults (<PREFIX>_<ACTION>_ABORTED), heals on a non-failing terminal state. Captures the terminal action verdict that the diagnostic and log bridges cannot see (#423). - Fault state is per-ACTION, derived from the whole
GoalStatusArrayon each message: a fault is raised only on thehealthy -> failedtransition and healed only onfailed -> healthy, so it is order-independent and resilient to dropped terminal messages. Per-goal dedup now only suppresses duplicate log lines. -
canceled_is_faultemits a status-aware<PREFIX>_<ACTION>_CANCELEDcode; such a fault heals on the next non-failing terminal or when the canceled goal ages out of the retained status array. - Vanished actions (lifecycle deactivate, one-shot nodes) are pruned on rescan.
- Parameters are range-checked/normalized at load; an incompatible action status QoS is now warned about instead of silently dropping faults.
- Ships a default configuration so the bridge starts out of the box (#459)
- Fault source is fixed at first report instead of being re-attributed later: faults raised before discovery settles register the server FQN, faults are never attributed to the discovery placeholder node name, and the subscriptions and timer are reset in the destructor (#466)
- Contributors: \@bburda, \@mfaferek93
Package Dependencies
System Dependencies
Dependant Packages
| Name | Deps |
|---|---|
| ros2_medkit_gateway |
Launch files
Messages
Services
Plugins
Recent questions tagged ros2_medkit_action_status_bridge at Robotics Stack Exchange
Package Summary
| Version | 0.6.0 |
| License | Apache-2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Checkout URI | https://github.com/selfpatch/ros2_medkit.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-07-07 |
| Dev Status | DEVELOPED |
| Released | RELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- mfaferek93
Authors
ros2_medkit_action_status_bridge
Generic drop-in bridge that turns terminal ROS 2 action goal states into
structured medkit faults. It watches the /<action>/_action/status
(action_msgs/msg/GoalStatusArray) topic that every action server
publishes, so it works across Nav2, MoveIt 2, ros2_control’s controller
actions, and any custom action with no per-project code.
Why this matters
For the core ROS 2 league the authoritative “the goal failed” verdict lives on
the action-result channel, not on /diagnostics or /rosout:
- Nav2
NavigateToPose->GoalStatus = ABORTED - MoveIt
MoveGroup->GoalStatus = ABORTED(withMoveItErrorCodein the result) - ros2_control controller actions -> aborted goals
Neither the diagnostic bridge nor the log bridge sees this. This bridge does, generically, by observing the goal-status topic.
What it does
- Discovers every action on the graph by scanning for
*/_action/statustopics (re-scanned on a timer to catch actions that appear later, to drop actions that vanish, e.g. a Nav2 lifecycle deactivate or a one-shot node, and to retry any fault whose delivery to the FaultManager was deferred). -
ABORTED (6)-> fault (SEVERITY_ERRORby default). -
CANCELED (5)-> fault only ifcanceled_is_fault(off by default; cancel is usually intentional). When enabled it emits a_CANCELEDcode. -
SUCCEEDED (4)->PASSEDto heal the action’s fault code (if enabled). -
fault_codeis<PREFIX>_<ACTION>_ABORTED(or_CANCELED), e.g.ACTION_NAVIGATE_TO_POSE_ABORTED.source_idis the action server’s node FQN (resolved from the status topic’s publisher, e.g./bt_navigator), so the fault associates with that node’s SOVD entity; it falls back to the action name only if no publisher is visible.
Per-action state, not per-goal
The fault is a property of the action, not of an individual goal. On every
status message the whole GoalStatusArray is scanned for the net state:
- if any goal is
ABORTED(orCANCELEDwhencanceled_is_fault), the action is failed (order of goals in the array does not matter); - otherwise, if the array has terminal goals and none are failing, the action is healthy.
A fault is raised only on the healthy -> failed transition and healed only on
failed -> healthy. This means one failed goal cannot heal an action while
another goal is still failed, and a dropped terminal message cannot leave a
fault stuck. Per-goal dedup (keyed on goal_id:status) only suppresses
duplicate log lines; it never gates the transition.
The bridge tracks the latest observed state (the desired state) separately from what the FaultManager was actually told. The transition is committed only once the report is delivered: if the FaultManager service is not discovered yet
- e.g. an action’s status topic is
transient_local(latched) and its terminal status reaches the bridge during the startup discovery window, before the report client connects - the transition stays pending and is retried, rather than the high-severity fault being silently dropped. (The latched status is delivered to a subscription only once, so without this retry the dropped report would never recover.) See Delivery latency and freeze-frame.
Scope: the terminal verdict, not the reason
This bridge delivers the generic “it aborted” event. The action-specific
reason (e.g. MoveItErrorCode.val = -26 START_STATE_IN_COLLISION, Nav2
error_code on Iron/Jazzy) lives in the action result message and is a separate
enrichment concern (a future action-result reader using runtime message
introspection / dynmsg). Per-project plugins are only needed for human-readable
labels, not to surface the fault.
Delivery latency and freeze-frame
The FaultManager captures the freeze-frame (topic snapshot, rosbag) when it receives the report, so the value of that context decays with delivery latency: a late fault is reported against state that has already moved on. The bridge therefore optimises for delivering a fault to the FaultManager as soon as physically possible.
- Happy path (service discovered): the fault is reported synchronously in the status callback, the instant the terminal status arrives. This is the normal runtime case and is already ASAP - nothing is deferred.
-
Degraded path (service not yet discovered): a fault cannot physically be
delivered before the FaultManager
report_faultservice is discovered (startup before the FaultManager is up, or a FaultManager restart). The transition is kept pending and retried on a dedicated fast retry timer (retry_period_sec, default 50 ms), decoupled from the slow discoveryrescan. So the fault lands within one short tick of the service appearing - keeping the freeze-frame as contemporaneous as the discovery floor allows.
We deliberately do not block the (single-threaded) executor waiting for the service: blocking would stall detection of other actions’ faults during an outage to shave the last few milliseconds off one fault - a bad trade when the freeze-frame is already only ~tens of ms fresher. The retry timer is armed only while a delivery is pending, so an idle bridge does no periodic work.
Known boundaries (narrow, and shrunk further by the fast retry; both are consequences of the level-triggered, net-state model, not silent loss in the common case):
File truncated at 100 lines see the full file
Changelog for package ros2_medkit_action_status_bridge
0.6.0 (2026-06-22)
- Initial release: generic action-status bridge. Watches every
/<action>/_action/statustopic and turns ABORTED goals into FaultManager faults (<PREFIX>_<ACTION>_ABORTED), heals on a non-failing terminal state. Captures the terminal action verdict that the diagnostic and log bridges cannot see (#423). - Fault state is per-ACTION, derived from the whole
GoalStatusArrayon each message: a fault is raised only on thehealthy -> failedtransition and healed only onfailed -> healthy, so it is order-independent and resilient to dropped terminal messages. Per-goal dedup now only suppresses duplicate log lines. -
canceled_is_faultemits a status-aware<PREFIX>_<ACTION>_CANCELEDcode; such a fault heals on the next non-failing terminal or when the canceled goal ages out of the retained status array. - Vanished actions (lifecycle deactivate, one-shot nodes) are pruned on rescan.
- Parameters are range-checked/normalized at load; an incompatible action status QoS is now warned about instead of silently dropping faults.
- Ships a default configuration so the bridge starts out of the box (#459)
- Fault source is fixed at first report instead of being re-attributed later: faults raised before discovery settles register the server FQN, faults are never attributed to the discovery placeholder node name, and the subscriptions and timer are reset in the destructor (#466)
- Contributors: \@bburda, \@mfaferek93
Package Dependencies
System Dependencies
Dependant Packages
| Name | Deps |
|---|---|
| ros2_medkit_gateway |
Launch files
Messages
Services
Plugins
Recent questions tagged ros2_medkit_action_status_bridge at Robotics Stack Exchange
Package Summary
| Version | 0.6.0 |
| License | Apache-2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Checkout URI | https://github.com/selfpatch/ros2_medkit.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-07-07 |
| Dev Status | DEVELOPED |
| Released | RELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- mfaferek93
Authors
ros2_medkit_action_status_bridge
Generic drop-in bridge that turns terminal ROS 2 action goal states into
structured medkit faults. It watches the /<action>/_action/status
(action_msgs/msg/GoalStatusArray) topic that every action server
publishes, so it works across Nav2, MoveIt 2, ros2_control’s controller
actions, and any custom action with no per-project code.
Why this matters
For the core ROS 2 league the authoritative “the goal failed” verdict lives on
the action-result channel, not on /diagnostics or /rosout:
- Nav2
NavigateToPose->GoalStatus = ABORTED - MoveIt
MoveGroup->GoalStatus = ABORTED(withMoveItErrorCodein the result) - ros2_control controller actions -> aborted goals
Neither the diagnostic bridge nor the log bridge sees this. This bridge does, generically, by observing the goal-status topic.
What it does
- Discovers every action on the graph by scanning for
*/_action/statustopics (re-scanned on a timer to catch actions that appear later, to drop actions that vanish, e.g. a Nav2 lifecycle deactivate or a one-shot node, and to retry any fault whose delivery to the FaultManager was deferred). -
ABORTED (6)-> fault (SEVERITY_ERRORby default). -
CANCELED (5)-> fault only ifcanceled_is_fault(off by default; cancel is usually intentional). When enabled it emits a_CANCELEDcode. -
SUCCEEDED (4)->PASSEDto heal the action’s fault code (if enabled). -
fault_codeis<PREFIX>_<ACTION>_ABORTED(or_CANCELED), e.g.ACTION_NAVIGATE_TO_POSE_ABORTED.source_idis the action server’s node FQN (resolved from the status topic’s publisher, e.g./bt_navigator), so the fault associates with that node’s SOVD entity; it falls back to the action name only if no publisher is visible.
Per-action state, not per-goal
The fault is a property of the action, not of an individual goal. On every
status message the whole GoalStatusArray is scanned for the net state:
- if any goal is
ABORTED(orCANCELEDwhencanceled_is_fault), the action is failed (order of goals in the array does not matter); - otherwise, if the array has terminal goals and none are failing, the action is healthy.
A fault is raised only on the healthy -> failed transition and healed only on
failed -> healthy. This means one failed goal cannot heal an action while
another goal is still failed, and a dropped terminal message cannot leave a
fault stuck. Per-goal dedup (keyed on goal_id:status) only suppresses
duplicate log lines; it never gates the transition.
The bridge tracks the latest observed state (the desired state) separately from what the FaultManager was actually told. The transition is committed only once the report is delivered: if the FaultManager service is not discovered yet
- e.g. an action’s status topic is
transient_local(latched) and its terminal status reaches the bridge during the startup discovery window, before the report client connects - the transition stays pending and is retried, rather than the high-severity fault being silently dropped. (The latched status is delivered to a subscription only once, so without this retry the dropped report would never recover.) See Delivery latency and freeze-frame.
Scope: the terminal verdict, not the reason
This bridge delivers the generic “it aborted” event. The action-specific
reason (e.g. MoveItErrorCode.val = -26 START_STATE_IN_COLLISION, Nav2
error_code on Iron/Jazzy) lives in the action result message and is a separate
enrichment concern (a future action-result reader using runtime message
introspection / dynmsg). Per-project plugins are only needed for human-readable
labels, not to surface the fault.
Delivery latency and freeze-frame
The FaultManager captures the freeze-frame (topic snapshot, rosbag) when it receives the report, so the value of that context decays with delivery latency: a late fault is reported against state that has already moved on. The bridge therefore optimises for delivering a fault to the FaultManager as soon as physically possible.
- Happy path (service discovered): the fault is reported synchronously in the status callback, the instant the terminal status arrives. This is the normal runtime case and is already ASAP - nothing is deferred.
-
Degraded path (service not yet discovered): a fault cannot physically be
delivered before the FaultManager
report_faultservice is discovered (startup before the FaultManager is up, or a FaultManager restart). The transition is kept pending and retried on a dedicated fast retry timer (retry_period_sec, default 50 ms), decoupled from the slow discoveryrescan. So the fault lands within one short tick of the service appearing - keeping the freeze-frame as contemporaneous as the discovery floor allows.
We deliberately do not block the (single-threaded) executor waiting for the service: blocking would stall detection of other actions’ faults during an outage to shave the last few milliseconds off one fault - a bad trade when the freeze-frame is already only ~tens of ms fresher. The retry timer is armed only while a delivery is pending, so an idle bridge does no periodic work.
Known boundaries (narrow, and shrunk further by the fast retry; both are consequences of the level-triggered, net-state model, not silent loss in the common case):
File truncated at 100 lines see the full file
Changelog for package ros2_medkit_action_status_bridge
0.6.0 (2026-06-22)
- Initial release: generic action-status bridge. Watches every
/<action>/_action/statustopic and turns ABORTED goals into FaultManager faults (<PREFIX>_<ACTION>_ABORTED), heals on a non-failing terminal state. Captures the terminal action verdict that the diagnostic and log bridges cannot see (#423). - Fault state is per-ACTION, derived from the whole
GoalStatusArrayon each message: a fault is raised only on thehealthy -> failedtransition and healed only onfailed -> healthy, so it is order-independent and resilient to dropped terminal messages. Per-goal dedup now only suppresses duplicate log lines. -
canceled_is_faultemits a status-aware<PREFIX>_<ACTION>_CANCELEDcode; such a fault heals on the next non-failing terminal or when the canceled goal ages out of the retained status array. - Vanished actions (lifecycle deactivate, one-shot nodes) are pruned on rescan.
- Parameters are range-checked/normalized at load; an incompatible action status QoS is now warned about instead of silently dropping faults.
- Ships a default configuration so the bridge starts out of the box (#459)
- Fault source is fixed at first report instead of being re-attributed later: faults raised before discovery settles register the server FQN, faults are never attributed to the discovery placeholder node name, and the subscriptions and timer are reset in the destructor (#466)
- Contributors: \@bburda, \@mfaferek93
Package Dependencies
System Dependencies
Dependant Packages
| Name | Deps |
|---|---|
| ros2_medkit_gateway |
Launch files
Messages
Services
Plugins
Recent questions tagged ros2_medkit_action_status_bridge at Robotics Stack Exchange
Package Summary
| Version | 0.6.0 |
| License | Apache-2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Checkout URI | https://github.com/selfpatch/ros2_medkit.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-07-07 |
| Dev Status | DEVELOPED |
| Released | RELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- mfaferek93
Authors
ros2_medkit_action_status_bridge
Generic drop-in bridge that turns terminal ROS 2 action goal states into
structured medkit faults. It watches the /<action>/_action/status
(action_msgs/msg/GoalStatusArray) topic that every action server
publishes, so it works across Nav2, MoveIt 2, ros2_control’s controller
actions, and any custom action with no per-project code.
Why this matters
For the core ROS 2 league the authoritative “the goal failed” verdict lives on
the action-result channel, not on /diagnostics or /rosout:
- Nav2
NavigateToPose->GoalStatus = ABORTED - MoveIt
MoveGroup->GoalStatus = ABORTED(withMoveItErrorCodein the result) - ros2_control controller actions -> aborted goals
Neither the diagnostic bridge nor the log bridge sees this. This bridge does, generically, by observing the goal-status topic.
What it does
- Discovers every action on the graph by scanning for
*/_action/statustopics (re-scanned on a timer to catch actions that appear later, to drop actions that vanish, e.g. a Nav2 lifecycle deactivate or a one-shot node, and to retry any fault whose delivery to the FaultManager was deferred). -
ABORTED (6)-> fault (SEVERITY_ERRORby default). -
CANCELED (5)-> fault only ifcanceled_is_fault(off by default; cancel is usually intentional). When enabled it emits a_CANCELEDcode. -
SUCCEEDED (4)->PASSEDto heal the action’s fault code (if enabled). -
fault_codeis<PREFIX>_<ACTION>_ABORTED(or_CANCELED), e.g.ACTION_NAVIGATE_TO_POSE_ABORTED.source_idis the action server’s node FQN (resolved from the status topic’s publisher, e.g./bt_navigator), so the fault associates with that node’s SOVD entity; it falls back to the action name only if no publisher is visible.
Per-action state, not per-goal
The fault is a property of the action, not of an individual goal. On every
status message the whole GoalStatusArray is scanned for the net state:
- if any goal is
ABORTED(orCANCELEDwhencanceled_is_fault), the action is failed (order of goals in the array does not matter); - otherwise, if the array has terminal goals and none are failing, the action is healthy.
A fault is raised only on the healthy -> failed transition and healed only on
failed -> healthy. This means one failed goal cannot heal an action while
another goal is still failed, and a dropped terminal message cannot leave a
fault stuck. Per-goal dedup (keyed on goal_id:status) only suppresses
duplicate log lines; it never gates the transition.
The bridge tracks the latest observed state (the desired state) separately from what the FaultManager was actually told. The transition is committed only once the report is delivered: if the FaultManager service is not discovered yet
- e.g. an action’s status topic is
transient_local(latched) and its terminal status reaches the bridge during the startup discovery window, before the report client connects - the transition stays pending and is retried, rather than the high-severity fault being silently dropped. (The latched status is delivered to a subscription only once, so without this retry the dropped report would never recover.) See Delivery latency and freeze-frame.
Scope: the terminal verdict, not the reason
This bridge delivers the generic “it aborted” event. The action-specific
reason (e.g. MoveItErrorCode.val = -26 START_STATE_IN_COLLISION, Nav2
error_code on Iron/Jazzy) lives in the action result message and is a separate
enrichment concern (a future action-result reader using runtime message
introspection / dynmsg). Per-project plugins are only needed for human-readable
labels, not to surface the fault.
Delivery latency and freeze-frame
The FaultManager captures the freeze-frame (topic snapshot, rosbag) when it receives the report, so the value of that context decays with delivery latency: a late fault is reported against state that has already moved on. The bridge therefore optimises for delivering a fault to the FaultManager as soon as physically possible.
- Happy path (service discovered): the fault is reported synchronously in the status callback, the instant the terminal status arrives. This is the normal runtime case and is already ASAP - nothing is deferred.
-
Degraded path (service not yet discovered): a fault cannot physically be
delivered before the FaultManager
report_faultservice is discovered (startup before the FaultManager is up, or a FaultManager restart). The transition is kept pending and retried on a dedicated fast retry timer (retry_period_sec, default 50 ms), decoupled from the slow discoveryrescan. So the fault lands within one short tick of the service appearing - keeping the freeze-frame as contemporaneous as the discovery floor allows.
We deliberately do not block the (single-threaded) executor waiting for the service: blocking would stall detection of other actions’ faults during an outage to shave the last few milliseconds off one fault - a bad trade when the freeze-frame is already only ~tens of ms fresher. The retry timer is armed only while a delivery is pending, so an idle bridge does no periodic work.
Known boundaries (narrow, and shrunk further by the fast retry; both are consequences of the level-triggered, net-state model, not silent loss in the common case):
File truncated at 100 lines see the full file
Changelog for package ros2_medkit_action_status_bridge
0.6.0 (2026-06-22)
- Initial release: generic action-status bridge. Watches every
/<action>/_action/statustopic and turns ABORTED goals into FaultManager faults (<PREFIX>_<ACTION>_ABORTED), heals on a non-failing terminal state. Captures the terminal action verdict that the diagnostic and log bridges cannot see (#423). - Fault state is per-ACTION, derived from the whole
GoalStatusArrayon each message: a fault is raised only on thehealthy -> failedtransition and healed only onfailed -> healthy, so it is order-independent and resilient to dropped terminal messages. Per-goal dedup now only suppresses duplicate log lines. -
canceled_is_faultemits a status-aware<PREFIX>_<ACTION>_CANCELEDcode; such a fault heals on the next non-failing terminal or when the canceled goal ages out of the retained status array. - Vanished actions (lifecycle deactivate, one-shot nodes) are pruned on rescan.
- Parameters are range-checked/normalized at load; an incompatible action status QoS is now warned about instead of silently dropping faults.
- Ships a default configuration so the bridge starts out of the box (#459)
- Fault source is fixed at first report instead of being re-attributed later: faults raised before discovery settles register the server FQN, faults are never attributed to the discovery placeholder node name, and the subscriptions and timer are reset in the destructor (#466)
- Contributors: \@bburda, \@mfaferek93
Package Dependencies
System Dependencies
Dependant Packages
| Name | Deps |
|---|---|
| ros2_medkit_gateway |
Launch files
Messages
Services
Plugins
Recent questions tagged ros2_medkit_action_status_bridge at Robotics Stack Exchange
Package Summary
| Version | 0.6.0 |
| License | Apache-2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Checkout URI | https://github.com/selfpatch/ros2_medkit.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-07-07 |
| Dev Status | DEVELOPED |
| Released | RELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- mfaferek93
Authors
ros2_medkit_action_status_bridge
Generic drop-in bridge that turns terminal ROS 2 action goal states into
structured medkit faults. It watches the /<action>/_action/status
(action_msgs/msg/GoalStatusArray) topic that every action server
publishes, so it works across Nav2, MoveIt 2, ros2_control’s controller
actions, and any custom action with no per-project code.
Why this matters
For the core ROS 2 league the authoritative “the goal failed” verdict lives on
the action-result channel, not on /diagnostics or /rosout:
- Nav2
NavigateToPose->GoalStatus = ABORTED - MoveIt
MoveGroup->GoalStatus = ABORTED(withMoveItErrorCodein the result) - ros2_control controller actions -> aborted goals
Neither the diagnostic bridge nor the log bridge sees this. This bridge does, generically, by observing the goal-status topic.
What it does
- Discovers every action on the graph by scanning for
*/_action/statustopics (re-scanned on a timer to catch actions that appear later, to drop actions that vanish, e.g. a Nav2 lifecycle deactivate or a one-shot node, and to retry any fault whose delivery to the FaultManager was deferred). -
ABORTED (6)-> fault (SEVERITY_ERRORby default). -
CANCELED (5)-> fault only ifcanceled_is_fault(off by default; cancel is usually intentional). When enabled it emits a_CANCELEDcode. -
SUCCEEDED (4)->PASSEDto heal the action’s fault code (if enabled). -
fault_codeis<PREFIX>_<ACTION>_ABORTED(or_CANCELED), e.g.ACTION_NAVIGATE_TO_POSE_ABORTED.source_idis the action server’s node FQN (resolved from the status topic’s publisher, e.g./bt_navigator), so the fault associates with that node’s SOVD entity; it falls back to the action name only if no publisher is visible.
Per-action state, not per-goal
The fault is a property of the action, not of an individual goal. On every
status message the whole GoalStatusArray is scanned for the net state:
- if any goal is
ABORTED(orCANCELEDwhencanceled_is_fault), the action is failed (order of goals in the array does not matter); - otherwise, if the array has terminal goals and none are failing, the action is healthy.
A fault is raised only on the healthy -> failed transition and healed only on
failed -> healthy. This means one failed goal cannot heal an action while
another goal is still failed, and a dropped terminal message cannot leave a
fault stuck. Per-goal dedup (keyed on goal_id:status) only suppresses
duplicate log lines; it never gates the transition.
The bridge tracks the latest observed state (the desired state) separately from what the FaultManager was actually told. The transition is committed only once the report is delivered: if the FaultManager service is not discovered yet
- e.g. an action’s status topic is
transient_local(latched) and its terminal status reaches the bridge during the startup discovery window, before the report client connects - the transition stays pending and is retried, rather than the high-severity fault being silently dropped. (The latched status is delivered to a subscription only once, so without this retry the dropped report would never recover.) See Delivery latency and freeze-frame.
Scope: the terminal verdict, not the reason
This bridge delivers the generic “it aborted” event. The action-specific
reason (e.g. MoveItErrorCode.val = -26 START_STATE_IN_COLLISION, Nav2
error_code on Iron/Jazzy) lives in the action result message and is a separate
enrichment concern (a future action-result reader using runtime message
introspection / dynmsg). Per-project plugins are only needed for human-readable
labels, not to surface the fault.
Delivery latency and freeze-frame
The FaultManager captures the freeze-frame (topic snapshot, rosbag) when it receives the report, so the value of that context decays with delivery latency: a late fault is reported against state that has already moved on. The bridge therefore optimises for delivering a fault to the FaultManager as soon as physically possible.
- Happy path (service discovered): the fault is reported synchronously in the status callback, the instant the terminal status arrives. This is the normal runtime case and is already ASAP - nothing is deferred.
-
Degraded path (service not yet discovered): a fault cannot physically be
delivered before the FaultManager
report_faultservice is discovered (startup before the FaultManager is up, or a FaultManager restart). The transition is kept pending and retried on a dedicated fast retry timer (retry_period_sec, default 50 ms), decoupled from the slow discoveryrescan. So the fault lands within one short tick of the service appearing - keeping the freeze-frame as contemporaneous as the discovery floor allows.
We deliberately do not block the (single-threaded) executor waiting for the service: blocking would stall detection of other actions’ faults during an outage to shave the last few milliseconds off one fault - a bad trade when the freeze-frame is already only ~tens of ms fresher. The retry timer is armed only while a delivery is pending, so an idle bridge does no periodic work.
Known boundaries (narrow, and shrunk further by the fast retry; both are consequences of the level-triggered, net-state model, not silent loss in the common case):
File truncated at 100 lines see the full file
Changelog for package ros2_medkit_action_status_bridge
0.6.0 (2026-06-22)
- Initial release: generic action-status bridge. Watches every
/<action>/_action/statustopic and turns ABORTED goals into FaultManager faults (<PREFIX>_<ACTION>_ABORTED), heals on a non-failing terminal state. Captures the terminal action verdict that the diagnostic and log bridges cannot see (#423). - Fault state is per-ACTION, derived from the whole
GoalStatusArrayon each message: a fault is raised only on thehealthy -> failedtransition and healed only onfailed -> healthy, so it is order-independent and resilient to dropped terminal messages. Per-goal dedup now only suppresses duplicate log lines. -
canceled_is_faultemits a status-aware<PREFIX>_<ACTION>_CANCELEDcode; such a fault heals on the next non-failing terminal or when the canceled goal ages out of the retained status array. - Vanished actions (lifecycle deactivate, one-shot nodes) are pruned on rescan.
- Parameters are range-checked/normalized at load; an incompatible action status QoS is now warned about instead of silently dropping faults.
- Ships a default configuration so the bridge starts out of the box (#459)
- Fault source is fixed at first report instead of being re-attributed later: faults raised before discovery settles register the server FQN, faults are never attributed to the discovery placeholder node name, and the subscriptions and timer are reset in the destructor (#466)
- Contributors: \@bburda, \@mfaferek93
Package Dependencies
System Dependencies
Dependant Packages
| Name | Deps |
|---|---|
| ros2_medkit_gateway |
Launch files
Messages
Services
Plugins
Recent questions tagged ros2_medkit_action_status_bridge at Robotics Stack Exchange
Package Summary
| Version | 0.6.0 |
| License | Apache-2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Checkout URI | https://github.com/selfpatch/ros2_medkit.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-07-07 |
| Dev Status | DEVELOPED |
| Released | RELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- mfaferek93
Authors
ros2_medkit_action_status_bridge
Generic drop-in bridge that turns terminal ROS 2 action goal states into
structured medkit faults. It watches the /<action>/_action/status
(action_msgs/msg/GoalStatusArray) topic that every action server
publishes, so it works across Nav2, MoveIt 2, ros2_control’s controller
actions, and any custom action with no per-project code.
Why this matters
For the core ROS 2 league the authoritative “the goal failed” verdict lives on
the action-result channel, not on /diagnostics or /rosout:
- Nav2
NavigateToPose->GoalStatus = ABORTED - MoveIt
MoveGroup->GoalStatus = ABORTED(withMoveItErrorCodein the result) - ros2_control controller actions -> aborted goals
Neither the diagnostic bridge nor the log bridge sees this. This bridge does, generically, by observing the goal-status topic.
What it does
- Discovers every action on the graph by scanning for
*/_action/statustopics (re-scanned on a timer to catch actions that appear later, to drop actions that vanish, e.g. a Nav2 lifecycle deactivate or a one-shot node, and to retry any fault whose delivery to the FaultManager was deferred). -
ABORTED (6)-> fault (SEVERITY_ERRORby default). -
CANCELED (5)-> fault only ifcanceled_is_fault(off by default; cancel is usually intentional). When enabled it emits a_CANCELEDcode. -
SUCCEEDED (4)->PASSEDto heal the action’s fault code (if enabled). -
fault_codeis<PREFIX>_<ACTION>_ABORTED(or_CANCELED), e.g.ACTION_NAVIGATE_TO_POSE_ABORTED.source_idis the action server’s node FQN (resolved from the status topic’s publisher, e.g./bt_navigator), so the fault associates with that node’s SOVD entity; it falls back to the action name only if no publisher is visible.
Per-action state, not per-goal
The fault is a property of the action, not of an individual goal. On every
status message the whole GoalStatusArray is scanned for the net state:
- if any goal is
ABORTED(orCANCELEDwhencanceled_is_fault), the action is failed (order of goals in the array does not matter); - otherwise, if the array has terminal goals and none are failing, the action is healthy.
A fault is raised only on the healthy -> failed transition and healed only on
failed -> healthy. This means one failed goal cannot heal an action while
another goal is still failed, and a dropped terminal message cannot leave a
fault stuck. Per-goal dedup (keyed on goal_id:status) only suppresses
duplicate log lines; it never gates the transition.
The bridge tracks the latest observed state (the desired state) separately from what the FaultManager was actually told. The transition is committed only once the report is delivered: if the FaultManager service is not discovered yet
- e.g. an action’s status topic is
transient_local(latched) and its terminal status reaches the bridge during the startup discovery window, before the report client connects - the transition stays pending and is retried, rather than the high-severity fault being silently dropped. (The latched status is delivered to a subscription only once, so without this retry the dropped report would never recover.) See Delivery latency and freeze-frame.
Scope: the terminal verdict, not the reason
This bridge delivers the generic “it aborted” event. The action-specific
reason (e.g. MoveItErrorCode.val = -26 START_STATE_IN_COLLISION, Nav2
error_code on Iron/Jazzy) lives in the action result message and is a separate
enrichment concern (a future action-result reader using runtime message
introspection / dynmsg). Per-project plugins are only needed for human-readable
labels, not to surface the fault.
Delivery latency and freeze-frame
The FaultManager captures the freeze-frame (topic snapshot, rosbag) when it receives the report, so the value of that context decays with delivery latency: a late fault is reported against state that has already moved on. The bridge therefore optimises for delivering a fault to the FaultManager as soon as physically possible.
- Happy path (service discovered): the fault is reported synchronously in the status callback, the instant the terminal status arrives. This is the normal runtime case and is already ASAP - nothing is deferred.
-
Degraded path (service not yet discovered): a fault cannot physically be
delivered before the FaultManager
report_faultservice is discovered (startup before the FaultManager is up, or a FaultManager restart). The transition is kept pending and retried on a dedicated fast retry timer (retry_period_sec, default 50 ms), decoupled from the slow discoveryrescan. So the fault lands within one short tick of the service appearing - keeping the freeze-frame as contemporaneous as the discovery floor allows.
We deliberately do not block the (single-threaded) executor waiting for the service: blocking would stall detection of other actions’ faults during an outage to shave the last few milliseconds off one fault - a bad trade when the freeze-frame is already only ~tens of ms fresher. The retry timer is armed only while a delivery is pending, so an idle bridge does no periodic work.
Known boundaries (narrow, and shrunk further by the fast retry; both are consequences of the level-triggered, net-state model, not silent loss in the common case):
File truncated at 100 lines see the full file
Changelog for package ros2_medkit_action_status_bridge
0.6.0 (2026-06-22)
- Initial release: generic action-status bridge. Watches every
/<action>/_action/statustopic and turns ABORTED goals into FaultManager faults (<PREFIX>_<ACTION>_ABORTED), heals on a non-failing terminal state. Captures the terminal action verdict that the diagnostic and log bridges cannot see (#423). - Fault state is per-ACTION, derived from the whole
GoalStatusArrayon each message: a fault is raised only on thehealthy -> failedtransition and healed only onfailed -> healthy, so it is order-independent and resilient to dropped terminal messages. Per-goal dedup now only suppresses duplicate log lines. -
canceled_is_faultemits a status-aware<PREFIX>_<ACTION>_CANCELEDcode; such a fault heals on the next non-failing terminal or when the canceled goal ages out of the retained status array. - Vanished actions (lifecycle deactivate, one-shot nodes) are pruned on rescan.
- Parameters are range-checked/normalized at load; an incompatible action status QoS is now warned about instead of silently dropping faults.
- Ships a default configuration so the bridge starts out of the box (#459)
- Fault source is fixed at first report instead of being re-attributed later: faults raised before discovery settles register the server FQN, faults are never attributed to the discovery placeholder node name, and the subscriptions and timer are reset in the destructor (#466)
- Contributors: \@bburda, \@mfaferek93
Package Dependencies
System Dependencies
Dependant Packages
| Name | Deps |
|---|---|
| ros2_medkit_gateway |
Launch files
Messages
Services
Plugins
Recent questions tagged ros2_medkit_action_status_bridge at Robotics Stack Exchange
Package Summary
| Version | 0.6.0 |
| License | Apache-2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Checkout URI | https://github.com/selfpatch/ros2_medkit.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-07-07 |
| Dev Status | DEVELOPED |
| Released | RELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- mfaferek93
Authors
ros2_medkit_action_status_bridge
Generic drop-in bridge that turns terminal ROS 2 action goal states into
structured medkit faults. It watches the /<action>/_action/status
(action_msgs/msg/GoalStatusArray) topic that every action server
publishes, so it works across Nav2, MoveIt 2, ros2_control’s controller
actions, and any custom action with no per-project code.
Why this matters
For the core ROS 2 league the authoritative “the goal failed” verdict lives on
the action-result channel, not on /diagnostics or /rosout:
- Nav2
NavigateToPose->GoalStatus = ABORTED - MoveIt
MoveGroup->GoalStatus = ABORTED(withMoveItErrorCodein the result) - ros2_control controller actions -> aborted goals
Neither the diagnostic bridge nor the log bridge sees this. This bridge does, generically, by observing the goal-status topic.
What it does
- Discovers every action on the graph by scanning for
*/_action/statustopics (re-scanned on a timer to catch actions that appear later, to drop actions that vanish, e.g. a Nav2 lifecycle deactivate or a one-shot node, and to retry any fault whose delivery to the FaultManager was deferred). -
ABORTED (6)-> fault (SEVERITY_ERRORby default). -
CANCELED (5)-> fault only ifcanceled_is_fault(off by default; cancel is usually intentional). When enabled it emits a_CANCELEDcode. -
SUCCEEDED (4)->PASSEDto heal the action’s fault code (if enabled). -
fault_codeis<PREFIX>_<ACTION>_ABORTED(or_CANCELED), e.g.ACTION_NAVIGATE_TO_POSE_ABORTED.source_idis the action server’s node FQN (resolved from the status topic’s publisher, e.g./bt_navigator), so the fault associates with that node’s SOVD entity; it falls back to the action name only if no publisher is visible.
Per-action state, not per-goal
The fault is a property of the action, not of an individual goal. On every
status message the whole GoalStatusArray is scanned for the net state:
- if any goal is
ABORTED(orCANCELEDwhencanceled_is_fault), the action is failed (order of goals in the array does not matter); - otherwise, if the array has terminal goals and none are failing, the action is healthy.
A fault is raised only on the healthy -> failed transition and healed only on
failed -> healthy. This means one failed goal cannot heal an action while
another goal is still failed, and a dropped terminal message cannot leave a
fault stuck. Per-goal dedup (keyed on goal_id:status) only suppresses
duplicate log lines; it never gates the transition.
The bridge tracks the latest observed state (the desired state) separately from what the FaultManager was actually told. The transition is committed only once the report is delivered: if the FaultManager service is not discovered yet
- e.g. an action’s status topic is
transient_local(latched) and its terminal status reaches the bridge during the startup discovery window, before the report client connects - the transition stays pending and is retried, rather than the high-severity fault being silently dropped. (The latched status is delivered to a subscription only once, so without this retry the dropped report would never recover.) See Delivery latency and freeze-frame.
Scope: the terminal verdict, not the reason
This bridge delivers the generic “it aborted” event. The action-specific
reason (e.g. MoveItErrorCode.val = -26 START_STATE_IN_COLLISION, Nav2
error_code on Iron/Jazzy) lives in the action result message and is a separate
enrichment concern (a future action-result reader using runtime message
introspection / dynmsg). Per-project plugins are only needed for human-readable
labels, not to surface the fault.
Delivery latency and freeze-frame
The FaultManager captures the freeze-frame (topic snapshot, rosbag) when it receives the report, so the value of that context decays with delivery latency: a late fault is reported against state that has already moved on. The bridge therefore optimises for delivering a fault to the FaultManager as soon as physically possible.
- Happy path (service discovered): the fault is reported synchronously in the status callback, the instant the terminal status arrives. This is the normal runtime case and is already ASAP - nothing is deferred.
-
Degraded path (service not yet discovered): a fault cannot physically be
delivered before the FaultManager
report_faultservice is discovered (startup before the FaultManager is up, or a FaultManager restart). The transition is kept pending and retried on a dedicated fast retry timer (retry_period_sec, default 50 ms), decoupled from the slow discoveryrescan. So the fault lands within one short tick of the service appearing - keeping the freeze-frame as contemporaneous as the discovery floor allows.
We deliberately do not block the (single-threaded) executor waiting for the service: blocking would stall detection of other actions’ faults during an outage to shave the last few milliseconds off one fault - a bad trade when the freeze-frame is already only ~tens of ms fresher. The retry timer is armed only while a delivery is pending, so an idle bridge does no periodic work.
Known boundaries (narrow, and shrunk further by the fast retry; both are consequences of the level-triggered, net-state model, not silent loss in the common case):
File truncated at 100 lines see the full file
Changelog for package ros2_medkit_action_status_bridge
0.6.0 (2026-06-22)
- Initial release: generic action-status bridge. Watches every
/<action>/_action/statustopic and turns ABORTED goals into FaultManager faults (<PREFIX>_<ACTION>_ABORTED), heals on a non-failing terminal state. Captures the terminal action verdict that the diagnostic and log bridges cannot see (#423). - Fault state is per-ACTION, derived from the whole
GoalStatusArrayon each message: a fault is raised only on thehealthy -> failedtransition and healed only onfailed -> healthy, so it is order-independent and resilient to dropped terminal messages. Per-goal dedup now only suppresses duplicate log lines. -
canceled_is_faultemits a status-aware<PREFIX>_<ACTION>_CANCELEDcode; such a fault heals on the next non-failing terminal or when the canceled goal ages out of the retained status array. - Vanished actions (lifecycle deactivate, one-shot nodes) are pruned on rescan.
- Parameters are range-checked/normalized at load; an incompatible action status QoS is now warned about instead of silently dropping faults.
- Ships a default configuration so the bridge starts out of the box (#459)
- Fault source is fixed at first report instead of being re-attributed later: faults raised before discovery settles register the server FQN, faults are never attributed to the discovery placeholder node name, and the subscriptions and timer are reset in the destructor (#466)
- Contributors: \@bburda, \@mfaferek93
Package Dependencies
System Dependencies
Dependant Packages
| Name | Deps |
|---|---|
| ros2_medkit_gateway |
Launch files
Messages
Services
Plugins
Recent questions tagged ros2_medkit_action_status_bridge at Robotics Stack Exchange
Package Summary
| Version | 0.6.0 |
| License | Apache-2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Checkout URI | https://github.com/selfpatch/ros2_medkit.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-07-07 |
| Dev Status | DEVELOPED |
| Released | RELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- mfaferek93
Authors
ros2_medkit_action_status_bridge
Generic drop-in bridge that turns terminal ROS 2 action goal states into
structured medkit faults. It watches the /<action>/_action/status
(action_msgs/msg/GoalStatusArray) topic that every action server
publishes, so it works across Nav2, MoveIt 2, ros2_control’s controller
actions, and any custom action with no per-project code.
Why this matters
For the core ROS 2 league the authoritative “the goal failed” verdict lives on
the action-result channel, not on /diagnostics or /rosout:
- Nav2
NavigateToPose->GoalStatus = ABORTED - MoveIt
MoveGroup->GoalStatus = ABORTED(withMoveItErrorCodein the result) - ros2_control controller actions -> aborted goals
Neither the diagnostic bridge nor the log bridge sees this. This bridge does, generically, by observing the goal-status topic.
What it does
- Discovers every action on the graph by scanning for
*/_action/statustopics (re-scanned on a timer to catch actions that appear later, to drop actions that vanish, e.g. a Nav2 lifecycle deactivate or a one-shot node, and to retry any fault whose delivery to the FaultManager was deferred). -
ABORTED (6)-> fault (SEVERITY_ERRORby default). -
CANCELED (5)-> fault only ifcanceled_is_fault(off by default; cancel is usually intentional). When enabled it emits a_CANCELEDcode. -
SUCCEEDED (4)->PASSEDto heal the action’s fault code (if enabled). -
fault_codeis<PREFIX>_<ACTION>_ABORTED(or_CANCELED), e.g.ACTION_NAVIGATE_TO_POSE_ABORTED.source_idis the action server’s node FQN (resolved from the status topic’s publisher, e.g./bt_navigator), so the fault associates with that node’s SOVD entity; it falls back to the action name only if no publisher is visible.
Per-action state, not per-goal
The fault is a property of the action, not of an individual goal. On every
status message the whole GoalStatusArray is scanned for the net state:
- if any goal is
ABORTED(orCANCELEDwhencanceled_is_fault), the action is failed (order of goals in the array does not matter); - otherwise, if the array has terminal goals and none are failing, the action is healthy.
A fault is raised only on the healthy -> failed transition and healed only on
failed -> healthy. This means one failed goal cannot heal an action while
another goal is still failed, and a dropped terminal message cannot leave a
fault stuck. Per-goal dedup (keyed on goal_id:status) only suppresses
duplicate log lines; it never gates the transition.
The bridge tracks the latest observed state (the desired state) separately from what the FaultManager was actually told. The transition is committed only once the report is delivered: if the FaultManager service is not discovered yet
- e.g. an action’s status topic is
transient_local(latched) and its terminal status reaches the bridge during the startup discovery window, before the report client connects - the transition stays pending and is retried, rather than the high-severity fault being silently dropped. (The latched status is delivered to a subscription only once, so without this retry the dropped report would never recover.) See Delivery latency and freeze-frame.
Scope: the terminal verdict, not the reason
This bridge delivers the generic “it aborted” event. The action-specific
reason (e.g. MoveItErrorCode.val = -26 START_STATE_IN_COLLISION, Nav2
error_code on Iron/Jazzy) lives in the action result message and is a separate
enrichment concern (a future action-result reader using runtime message
introspection / dynmsg). Per-project plugins are only needed for human-readable
labels, not to surface the fault.
Delivery latency and freeze-frame
The FaultManager captures the freeze-frame (topic snapshot, rosbag) when it receives the report, so the value of that context decays with delivery latency: a late fault is reported against state that has already moved on. The bridge therefore optimises for delivering a fault to the FaultManager as soon as physically possible.
- Happy path (service discovered): the fault is reported synchronously in the status callback, the instant the terminal status arrives. This is the normal runtime case and is already ASAP - nothing is deferred.
-
Degraded path (service not yet discovered): a fault cannot physically be
delivered before the FaultManager
report_faultservice is discovered (startup before the FaultManager is up, or a FaultManager restart). The transition is kept pending and retried on a dedicated fast retry timer (retry_period_sec, default 50 ms), decoupled from the slow discoveryrescan. So the fault lands within one short tick of the service appearing - keeping the freeze-frame as contemporaneous as the discovery floor allows.
We deliberately do not block the (single-threaded) executor waiting for the service: blocking would stall detection of other actions’ faults during an outage to shave the last few milliseconds off one fault - a bad trade when the freeze-frame is already only ~tens of ms fresher. The retry timer is armed only while a delivery is pending, so an idle bridge does no periodic work.
Known boundaries (narrow, and shrunk further by the fast retry; both are consequences of the level-triggered, net-state model, not silent loss in the common case):
File truncated at 100 lines see the full file
Changelog for package ros2_medkit_action_status_bridge
0.6.0 (2026-06-22)
- Initial release: generic action-status bridge. Watches every
/<action>/_action/statustopic and turns ABORTED goals into FaultManager faults (<PREFIX>_<ACTION>_ABORTED), heals on a non-failing terminal state. Captures the terminal action verdict that the diagnostic and log bridges cannot see (#423). - Fault state is per-ACTION, derived from the whole
GoalStatusArrayon each message: a fault is raised only on thehealthy -> failedtransition and healed only onfailed -> healthy, so it is order-independent and resilient to dropped terminal messages. Per-goal dedup now only suppresses duplicate log lines. -
canceled_is_faultemits a status-aware<PREFIX>_<ACTION>_CANCELEDcode; such a fault heals on the next non-failing terminal or when the canceled goal ages out of the retained status array. - Vanished actions (lifecycle deactivate, one-shot nodes) are pruned on rescan.
- Parameters are range-checked/normalized at load; an incompatible action status QoS is now warned about instead of silently dropping faults.
- Ships a default configuration so the bridge starts out of the box (#459)
- Fault source is fixed at first report instead of being re-attributed later: faults raised before discovery settles register the server FQN, faults are never attributed to the discovery placeholder node name, and the subscriptions and timer are reset in the destructor (#466)
- Contributors: \@bburda, \@mfaferek93
Package Dependencies
System Dependencies
Dependant Packages
| Name | Deps |
|---|---|
| ros2_medkit_gateway |
Launch files
Messages
Services
Plugins
Recent questions tagged ros2_medkit_action_status_bridge at Robotics Stack Exchange
Package Summary
| Version | 0.6.0 |
| License | Apache-2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Checkout URI | https://github.com/selfpatch/ros2_medkit.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-07-07 |
| Dev Status | DEVELOPED |
| Released | RELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- mfaferek93
Authors
ros2_medkit_action_status_bridge
Generic drop-in bridge that turns terminal ROS 2 action goal states into
structured medkit faults. It watches the /<action>/_action/status
(action_msgs/msg/GoalStatusArray) topic that every action server
publishes, so it works across Nav2, MoveIt 2, ros2_control’s controller
actions, and any custom action with no per-project code.
Why this matters
For the core ROS 2 league the authoritative “the goal failed” verdict lives on
the action-result channel, not on /diagnostics or /rosout:
- Nav2
NavigateToPose->GoalStatus = ABORTED - MoveIt
MoveGroup->GoalStatus = ABORTED(withMoveItErrorCodein the result) - ros2_control controller actions -> aborted goals
Neither the diagnostic bridge nor the log bridge sees this. This bridge does, generically, by observing the goal-status topic.
What it does
- Discovers every action on the graph by scanning for
*/_action/statustopics (re-scanned on a timer to catch actions that appear later, to drop actions that vanish, e.g. a Nav2 lifecycle deactivate or a one-shot node, and to retry any fault whose delivery to the FaultManager was deferred). -
ABORTED (6)-> fault (SEVERITY_ERRORby default). -
CANCELED (5)-> fault only ifcanceled_is_fault(off by default; cancel is usually intentional). When enabled it emits a_CANCELEDcode. -
SUCCEEDED (4)->PASSEDto heal the action’s fault code (if enabled). -
fault_codeis<PREFIX>_<ACTION>_ABORTED(or_CANCELED), e.g.ACTION_NAVIGATE_TO_POSE_ABORTED.source_idis the action server’s node FQN (resolved from the status topic’s publisher, e.g./bt_navigator), so the fault associates with that node’s SOVD entity; it falls back to the action name only if no publisher is visible.
Per-action state, not per-goal
The fault is a property of the action, not of an individual goal. On every
status message the whole GoalStatusArray is scanned for the net state:
- if any goal is
ABORTED(orCANCELEDwhencanceled_is_fault), the action is failed (order of goals in the array does not matter); - otherwise, if the array has terminal goals and none are failing, the action is healthy.
A fault is raised only on the healthy -> failed transition and healed only on
failed -> healthy. This means one failed goal cannot heal an action while
another goal is still failed, and a dropped terminal message cannot leave a
fault stuck. Per-goal dedup (keyed on goal_id:status) only suppresses
duplicate log lines; it never gates the transition.
The bridge tracks the latest observed state (the desired state) separately from what the FaultManager was actually told. The transition is committed only once the report is delivered: if the FaultManager service is not discovered yet
- e.g. an action’s status topic is
transient_local(latched) and its terminal status reaches the bridge during the startup discovery window, before the report client connects - the transition stays pending and is retried, rather than the high-severity fault being silently dropped. (The latched status is delivered to a subscription only once, so without this retry the dropped report would never recover.) See Delivery latency and freeze-frame.
Scope: the terminal verdict, not the reason
This bridge delivers the generic “it aborted” event. The action-specific
reason (e.g. MoveItErrorCode.val = -26 START_STATE_IN_COLLISION, Nav2
error_code on Iron/Jazzy) lives in the action result message and is a separate
enrichment concern (a future action-result reader using runtime message
introspection / dynmsg). Per-project plugins are only needed for human-readable
labels, not to surface the fault.
Delivery latency and freeze-frame
The FaultManager captures the freeze-frame (topic snapshot, rosbag) when it receives the report, so the value of that context decays with delivery latency: a late fault is reported against state that has already moved on. The bridge therefore optimises for delivering a fault to the FaultManager as soon as physically possible.
- Happy path (service discovered): the fault is reported synchronously in the status callback, the instant the terminal status arrives. This is the normal runtime case and is already ASAP - nothing is deferred.
-
Degraded path (service not yet discovered): a fault cannot physically be
delivered before the FaultManager
report_faultservice is discovered (startup before the FaultManager is up, or a FaultManager restart). The transition is kept pending and retried on a dedicated fast retry timer (retry_period_sec, default 50 ms), decoupled from the slow discoveryrescan. So the fault lands within one short tick of the service appearing - keeping the freeze-frame as contemporaneous as the discovery floor allows.
We deliberately do not block the (single-threaded) executor waiting for the service: blocking would stall detection of other actions’ faults during an outage to shave the last few milliseconds off one fault - a bad trade when the freeze-frame is already only ~tens of ms fresher. The retry timer is armed only while a delivery is pending, so an idle bridge does no periodic work.
Known boundaries (narrow, and shrunk further by the fast retry; both are consequences of the level-triggered, net-state model, not silent loss in the common case):
File truncated at 100 lines see the full file
Changelog for package ros2_medkit_action_status_bridge
0.6.0 (2026-06-22)
- Initial release: generic action-status bridge. Watches every
/<action>/_action/statustopic and turns ABORTED goals into FaultManager faults (<PREFIX>_<ACTION>_ABORTED), heals on a non-failing terminal state. Captures the terminal action verdict that the diagnostic and log bridges cannot see (#423). - Fault state is per-ACTION, derived from the whole
GoalStatusArrayon each message: a fault is raised only on thehealthy -> failedtransition and healed only onfailed -> healthy, so it is order-independent and resilient to dropped terminal messages. Per-goal dedup now only suppresses duplicate log lines. -
canceled_is_faultemits a status-aware<PREFIX>_<ACTION>_CANCELEDcode; such a fault heals on the next non-failing terminal or when the canceled goal ages out of the retained status array. - Vanished actions (lifecycle deactivate, one-shot nodes) are pruned on rescan.
- Parameters are range-checked/normalized at load; an incompatible action status QoS is now warned about instead of silently dropping faults.
- Ships a default configuration so the bridge starts out of the box (#459)
- Fault source is fixed at first report instead of being re-attributed later: faults raised before discovery settles register the server FQN, faults are never attributed to the discovery placeholder node name, and the subscriptions and timer are reset in the destructor (#466)
- Contributors: \@bburda, \@mfaferek93
Package Dependencies
System Dependencies
Dependant Packages
| Name | Deps |
|---|---|
| ros2_medkit_gateway |
Launch files
Messages
Services
Plugins
Recent questions tagged ros2_medkit_action_status_bridge at Robotics Stack Exchange
Package Summary
| Version | 0.6.0 |
| License | Apache-2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Checkout URI | https://github.com/selfpatch/ros2_medkit.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-07-07 |
| Dev Status | DEVELOPED |
| Released | RELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- mfaferek93
Authors
ros2_medkit_action_status_bridge
Generic drop-in bridge that turns terminal ROS 2 action goal states into
structured medkit faults. It watches the /<action>/_action/status
(action_msgs/msg/GoalStatusArray) topic that every action server
publishes, so it works across Nav2, MoveIt 2, ros2_control’s controller
actions, and any custom action with no per-project code.
Why this matters
For the core ROS 2 league the authoritative “the goal failed” verdict lives on
the action-result channel, not on /diagnostics or /rosout:
- Nav2
NavigateToPose->GoalStatus = ABORTED - MoveIt
MoveGroup->GoalStatus = ABORTED(withMoveItErrorCodein the result) - ros2_control controller actions -> aborted goals
Neither the diagnostic bridge nor the log bridge sees this. This bridge does, generically, by observing the goal-status topic.
What it does
- Discovers every action on the graph by scanning for
*/_action/statustopics (re-scanned on a timer to catch actions that appear later, to drop actions that vanish, e.g. a Nav2 lifecycle deactivate or a one-shot node, and to retry any fault whose delivery to the FaultManager was deferred). -
ABORTED (6)-> fault (SEVERITY_ERRORby default). -
CANCELED (5)-> fault only ifcanceled_is_fault(off by default; cancel is usually intentional). When enabled it emits a_CANCELEDcode. -
SUCCEEDED (4)->PASSEDto heal the action’s fault code (if enabled). -
fault_codeis<PREFIX>_<ACTION>_ABORTED(or_CANCELED), e.g.ACTION_NAVIGATE_TO_POSE_ABORTED.source_idis the action server’s node FQN (resolved from the status topic’s publisher, e.g./bt_navigator), so the fault associates with that node’s SOVD entity; it falls back to the action name only if no publisher is visible.
Per-action state, not per-goal
The fault is a property of the action, not of an individual goal. On every
status message the whole GoalStatusArray is scanned for the net state:
- if any goal is
ABORTED(orCANCELEDwhencanceled_is_fault), the action is failed (order of goals in the array does not matter); - otherwise, if the array has terminal goals and none are failing, the action is healthy.
A fault is raised only on the healthy -> failed transition and healed only on
failed -> healthy. This means one failed goal cannot heal an action while
another goal is still failed, and a dropped terminal message cannot leave a
fault stuck. Per-goal dedup (keyed on goal_id:status) only suppresses
duplicate log lines; it never gates the transition.
The bridge tracks the latest observed state (the desired state) separately from what the FaultManager was actually told. The transition is committed only once the report is delivered: if the FaultManager service is not discovered yet
- e.g. an action’s status topic is
transient_local(latched) and its terminal status reaches the bridge during the startup discovery window, before the report client connects - the transition stays pending and is retried, rather than the high-severity fault being silently dropped. (The latched status is delivered to a subscription only once, so without this retry the dropped report would never recover.) See Delivery latency and freeze-frame.
Scope: the terminal verdict, not the reason
This bridge delivers the generic “it aborted” event. The action-specific
reason (e.g. MoveItErrorCode.val = -26 START_STATE_IN_COLLISION, Nav2
error_code on Iron/Jazzy) lives in the action result message and is a separate
enrichment concern (a future action-result reader using runtime message
introspection / dynmsg). Per-project plugins are only needed for human-readable
labels, not to surface the fault.
Delivery latency and freeze-frame
The FaultManager captures the freeze-frame (topic snapshot, rosbag) when it receives the report, so the value of that context decays with delivery latency: a late fault is reported against state that has already moved on. The bridge therefore optimises for delivering a fault to the FaultManager as soon as physically possible.
- Happy path (service discovered): the fault is reported synchronously in the status callback, the instant the terminal status arrives. This is the normal runtime case and is already ASAP - nothing is deferred.
-
Degraded path (service not yet discovered): a fault cannot physically be
delivered before the FaultManager
report_faultservice is discovered (startup before the FaultManager is up, or a FaultManager restart). The transition is kept pending and retried on a dedicated fast retry timer (retry_period_sec, default 50 ms), decoupled from the slow discoveryrescan. So the fault lands within one short tick of the service appearing - keeping the freeze-frame as contemporaneous as the discovery floor allows.
We deliberately do not block the (single-threaded) executor waiting for the service: blocking would stall detection of other actions’ faults during an outage to shave the last few milliseconds off one fault - a bad trade when the freeze-frame is already only ~tens of ms fresher. The retry timer is armed only while a delivery is pending, so an idle bridge does no periodic work.
Known boundaries (narrow, and shrunk further by the fast retry; both are consequences of the level-triggered, net-state model, not silent loss in the common case):
File truncated at 100 lines see the full file
Changelog for package ros2_medkit_action_status_bridge
0.6.0 (2026-06-22)
- Initial release: generic action-status bridge. Watches every
/<action>/_action/statustopic and turns ABORTED goals into FaultManager faults (<PREFIX>_<ACTION>_ABORTED), heals on a non-failing terminal state. Captures the terminal action verdict that the diagnostic and log bridges cannot see (#423). - Fault state is per-ACTION, derived from the whole
GoalStatusArrayon each message: a fault is raised only on thehealthy -> failedtransition and healed only onfailed -> healthy, so it is order-independent and resilient to dropped terminal messages. Per-goal dedup now only suppresses duplicate log lines. -
canceled_is_faultemits a status-aware<PREFIX>_<ACTION>_CANCELEDcode; such a fault heals on the next non-failing terminal or when the canceled goal ages out of the retained status array. - Vanished actions (lifecycle deactivate, one-shot nodes) are pruned on rescan.
- Parameters are range-checked/normalized at load; an incompatible action status QoS is now warned about instead of silently dropping faults.
- Ships a default configuration so the bridge starts out of the box (#459)
- Fault source is fixed at first report instead of being re-attributed later: faults raised before discovery settles register the server FQN, faults are never attributed to the discovery placeholder node name, and the subscriptions and timer are reset in the destructor (#466)
- Contributors: \@bburda, \@mfaferek93
Package Dependencies
System Dependencies
Dependant Packages
| Name | Deps |
|---|---|
| ros2_medkit_gateway |
Launch files
Messages
Services
Plugins
Recent questions tagged ros2_medkit_action_status_bridge at Robotics Stack Exchange
Package Summary
| Version | 0.6.0 |
| License | Apache-2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Checkout URI | https://github.com/selfpatch/ros2_medkit.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-07-07 |
| Dev Status | DEVELOPED |
| Released | RELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- mfaferek93
Authors
ros2_medkit_action_status_bridge
Generic drop-in bridge that turns terminal ROS 2 action goal states into
structured medkit faults. It watches the /<action>/_action/status
(action_msgs/msg/GoalStatusArray) topic that every action server
publishes, so it works across Nav2, MoveIt 2, ros2_control’s controller
actions, and any custom action with no per-project code.
Why this matters
For the core ROS 2 league the authoritative “the goal failed” verdict lives on
the action-result channel, not on /diagnostics or /rosout:
- Nav2
NavigateToPose->GoalStatus = ABORTED - MoveIt
MoveGroup->GoalStatus = ABORTED(withMoveItErrorCodein the result) - ros2_control controller actions -> aborted goals
Neither the diagnostic bridge nor the log bridge sees this. This bridge does, generically, by observing the goal-status topic.
What it does
- Discovers every action on the graph by scanning for
*/_action/statustopics (re-scanned on a timer to catch actions that appear later, to drop actions that vanish, e.g. a Nav2 lifecycle deactivate or a one-shot node, and to retry any fault whose delivery to the FaultManager was deferred). -
ABORTED (6)-> fault (SEVERITY_ERRORby default). -
CANCELED (5)-> fault only ifcanceled_is_fault(off by default; cancel is usually intentional). When enabled it emits a_CANCELEDcode. -
SUCCEEDED (4)->PASSEDto heal the action’s fault code (if enabled). -
fault_codeis<PREFIX>_<ACTION>_ABORTED(or_CANCELED), e.g.ACTION_NAVIGATE_TO_POSE_ABORTED.source_idis the action server’s node FQN (resolved from the status topic’s publisher, e.g./bt_navigator), so the fault associates with that node’s SOVD entity; it falls back to the action name only if no publisher is visible.
Per-action state, not per-goal
The fault is a property of the action, not of an individual goal. On every
status message the whole GoalStatusArray is scanned for the net state:
- if any goal is
ABORTED(orCANCELEDwhencanceled_is_fault), the action is failed (order of goals in the array does not matter); - otherwise, if the array has terminal goals and none are failing, the action is healthy.
A fault is raised only on the healthy -> failed transition and healed only on
failed -> healthy. This means one failed goal cannot heal an action while
another goal is still failed, and a dropped terminal message cannot leave a
fault stuck. Per-goal dedup (keyed on goal_id:status) only suppresses
duplicate log lines; it never gates the transition.
The bridge tracks the latest observed state (the desired state) separately from what the FaultManager was actually told. The transition is committed only once the report is delivered: if the FaultManager service is not discovered yet
- e.g. an action’s status topic is
transient_local(latched) and its terminal status reaches the bridge during the startup discovery window, before the report client connects - the transition stays pending and is retried, rather than the high-severity fault being silently dropped. (The latched status is delivered to a subscription only once, so without this retry the dropped report would never recover.) See Delivery latency and freeze-frame.
Scope: the terminal verdict, not the reason
This bridge delivers the generic “it aborted” event. The action-specific
reason (e.g. MoveItErrorCode.val = -26 START_STATE_IN_COLLISION, Nav2
error_code on Iron/Jazzy) lives in the action result message and is a separate
enrichment concern (a future action-result reader using runtime message
introspection / dynmsg). Per-project plugins are only needed for human-readable
labels, not to surface the fault.
Delivery latency and freeze-frame
The FaultManager captures the freeze-frame (topic snapshot, rosbag) when it receives the report, so the value of that context decays with delivery latency: a late fault is reported against state that has already moved on. The bridge therefore optimises for delivering a fault to the FaultManager as soon as physically possible.
- Happy path (service discovered): the fault is reported synchronously in the status callback, the instant the terminal status arrives. This is the normal runtime case and is already ASAP - nothing is deferred.
-
Degraded path (service not yet discovered): a fault cannot physically be
delivered before the FaultManager
report_faultservice is discovered (startup before the FaultManager is up, or a FaultManager restart). The transition is kept pending and retried on a dedicated fast retry timer (retry_period_sec, default 50 ms), decoupled from the slow discoveryrescan. So the fault lands within one short tick of the service appearing - keeping the freeze-frame as contemporaneous as the discovery floor allows.
We deliberately do not block the (single-threaded) executor waiting for the service: blocking would stall detection of other actions’ faults during an outage to shave the last few milliseconds off one fault - a bad trade when the freeze-frame is already only ~tens of ms fresher. The retry timer is armed only while a delivery is pending, so an idle bridge does no periodic work.
Known boundaries (narrow, and shrunk further by the fast retry; both are consequences of the level-triggered, net-state model, not silent loss in the common case):
File truncated at 100 lines see the full file
Changelog for package ros2_medkit_action_status_bridge
0.6.0 (2026-06-22)
- Initial release: generic action-status bridge. Watches every
/<action>/_action/statustopic and turns ABORTED goals into FaultManager faults (<PREFIX>_<ACTION>_ABORTED), heals on a non-failing terminal state. Captures the terminal action verdict that the diagnostic and log bridges cannot see (#423). - Fault state is per-ACTION, derived from the whole
GoalStatusArrayon each message: a fault is raised only on thehealthy -> failedtransition and healed only onfailed -> healthy, so it is order-independent and resilient to dropped terminal messages. Per-goal dedup now only suppresses duplicate log lines. -
canceled_is_faultemits a status-aware<PREFIX>_<ACTION>_CANCELEDcode; such a fault heals on the next non-failing terminal or when the canceled goal ages out of the retained status array. - Vanished actions (lifecycle deactivate, one-shot nodes) are pruned on rescan.
- Parameters are range-checked/normalized at load; an incompatible action status QoS is now warned about instead of silently dropping faults.
- Ships a default configuration so the bridge starts out of the box (#459)
- Fault source is fixed at first report instead of being re-attributed later: faults raised before discovery settles register the server FQN, faults are never attributed to the discovery placeholder node name, and the subscriptions and timer are reset in the destructor (#466)
- Contributors: \@bburda, \@mfaferek93
Package Dependencies
System Dependencies
Dependant Packages
| Name | Deps |
|---|---|
| ros2_medkit_gateway |
Launch files
Messages
Services
Plugins
Recent questions tagged ros2_medkit_action_status_bridge at Robotics Stack Exchange
Package Summary
| Version | 0.6.0 |
| License | Apache-2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Checkout URI | https://github.com/selfpatch/ros2_medkit.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-07-07 |
| Dev Status | DEVELOPED |
| Released | RELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- mfaferek93
Authors
ros2_medkit_action_status_bridge
Generic drop-in bridge that turns terminal ROS 2 action goal states into
structured medkit faults. It watches the /<action>/_action/status
(action_msgs/msg/GoalStatusArray) topic that every action server
publishes, so it works across Nav2, MoveIt 2, ros2_control’s controller
actions, and any custom action with no per-project code.
Why this matters
For the core ROS 2 league the authoritative “the goal failed” verdict lives on
the action-result channel, not on /diagnostics or /rosout:
- Nav2
NavigateToPose->GoalStatus = ABORTED - MoveIt
MoveGroup->GoalStatus = ABORTED(withMoveItErrorCodein the result) - ros2_control controller actions -> aborted goals
Neither the diagnostic bridge nor the log bridge sees this. This bridge does, generically, by observing the goal-status topic.
What it does
- Discovers every action on the graph by scanning for
*/_action/statustopics (re-scanned on a timer to catch actions that appear later, to drop actions that vanish, e.g. a Nav2 lifecycle deactivate or a one-shot node, and to retry any fault whose delivery to the FaultManager was deferred). -
ABORTED (6)-> fault (SEVERITY_ERRORby default). -
CANCELED (5)-> fault only ifcanceled_is_fault(off by default; cancel is usually intentional). When enabled it emits a_CANCELEDcode. -
SUCCEEDED (4)->PASSEDto heal the action’s fault code (if enabled). -
fault_codeis<PREFIX>_<ACTION>_ABORTED(or_CANCELED), e.g.ACTION_NAVIGATE_TO_POSE_ABORTED.source_idis the action server’s node FQN (resolved from the status topic’s publisher, e.g./bt_navigator), so the fault associates with that node’s SOVD entity; it falls back to the action name only if no publisher is visible.
Per-action state, not per-goal
The fault is a property of the action, not of an individual goal. On every
status message the whole GoalStatusArray is scanned for the net state:
- if any goal is
ABORTED(orCANCELEDwhencanceled_is_fault), the action is failed (order of goals in the array does not matter); - otherwise, if the array has terminal goals and none are failing, the action is healthy.
A fault is raised only on the healthy -> failed transition and healed only on
failed -> healthy. This means one failed goal cannot heal an action while
another goal is still failed, and a dropped terminal message cannot leave a
fault stuck. Per-goal dedup (keyed on goal_id:status) only suppresses
duplicate log lines; it never gates the transition.
The bridge tracks the latest observed state (the desired state) separately from what the FaultManager was actually told. The transition is committed only once the report is delivered: if the FaultManager service is not discovered yet
- e.g. an action’s status topic is
transient_local(latched) and its terminal status reaches the bridge during the startup discovery window, before the report client connects - the transition stays pending and is retried, rather than the high-severity fault being silently dropped. (The latched status is delivered to a subscription only once, so without this retry the dropped report would never recover.) See Delivery latency and freeze-frame.
Scope: the terminal verdict, not the reason
This bridge delivers the generic “it aborted” event. The action-specific
reason (e.g. MoveItErrorCode.val = -26 START_STATE_IN_COLLISION, Nav2
error_code on Iron/Jazzy) lives in the action result message and is a separate
enrichment concern (a future action-result reader using runtime message
introspection / dynmsg). Per-project plugins are only needed for human-readable
labels, not to surface the fault.
Delivery latency and freeze-frame
The FaultManager captures the freeze-frame (topic snapshot, rosbag) when it receives the report, so the value of that context decays with delivery latency: a late fault is reported against state that has already moved on. The bridge therefore optimises for delivering a fault to the FaultManager as soon as physically possible.
- Happy path (service discovered): the fault is reported synchronously in the status callback, the instant the terminal status arrives. This is the normal runtime case and is already ASAP - nothing is deferred.
-
Degraded path (service not yet discovered): a fault cannot physically be
delivered before the FaultManager
report_faultservice is discovered (startup before the FaultManager is up, or a FaultManager restart). The transition is kept pending and retried on a dedicated fast retry timer (retry_period_sec, default 50 ms), decoupled from the slow discoveryrescan. So the fault lands within one short tick of the service appearing - keeping the freeze-frame as contemporaneous as the discovery floor allows.
We deliberately do not block the (single-threaded) executor waiting for the service: blocking would stall detection of other actions’ faults during an outage to shave the last few milliseconds off one fault - a bad trade when the freeze-frame is already only ~tens of ms fresher. The retry timer is armed only while a delivery is pending, so an idle bridge does no periodic work.
Known boundaries (narrow, and shrunk further by the fast retry; both are consequences of the level-triggered, net-state model, not silent loss in the common case):
File truncated at 100 lines see the full file
Changelog for package ros2_medkit_action_status_bridge
0.6.0 (2026-06-22)
- Initial release: generic action-status bridge. Watches every
/<action>/_action/statustopic and turns ABORTED goals into FaultManager faults (<PREFIX>_<ACTION>_ABORTED), heals on a non-failing terminal state. Captures the terminal action verdict that the diagnostic and log bridges cannot see (#423). - Fault state is per-ACTION, derived from the whole
GoalStatusArrayon each message: a fault is raised only on thehealthy -> failedtransition and healed only onfailed -> healthy, so it is order-independent and resilient to dropped terminal messages. Per-goal dedup now only suppresses duplicate log lines. -
canceled_is_faultemits a status-aware<PREFIX>_<ACTION>_CANCELEDcode; such a fault heals on the next non-failing terminal or when the canceled goal ages out of the retained status array. - Vanished actions (lifecycle deactivate, one-shot nodes) are pruned on rescan.
- Parameters are range-checked/normalized at load; an incompatible action status QoS is now warned about instead of silently dropping faults.
- Ships a default configuration so the bridge starts out of the box (#459)
- Fault source is fixed at first report instead of being re-attributed later: faults raised before discovery settles register the server FQN, faults are never attributed to the discovery placeholder node name, and the subscriptions and timer are reset in the destructor (#466)
- Contributors: \@bburda, \@mfaferek93
Package Dependencies
System Dependencies
Dependant Packages
| Name | Deps |
|---|---|
| ros2_medkit_gateway |
Launch files
Messages
Services
Plugins
Recent questions tagged ros2_medkit_action_status_bridge at Robotics Stack Exchange
Package Summary
| Version | 0.6.0 |
| License | Apache-2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Checkout URI | https://github.com/selfpatch/ros2_medkit.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-07-07 |
| Dev Status | DEVELOPED |
| Released | RELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- mfaferek93
Authors
ros2_medkit_action_status_bridge
Generic drop-in bridge that turns terminal ROS 2 action goal states into
structured medkit faults. It watches the /<action>/_action/status
(action_msgs/msg/GoalStatusArray) topic that every action server
publishes, so it works across Nav2, MoveIt 2, ros2_control’s controller
actions, and any custom action with no per-project code.
Why this matters
For the core ROS 2 league the authoritative “the goal failed” verdict lives on
the action-result channel, not on /diagnostics or /rosout:
- Nav2
NavigateToPose->GoalStatus = ABORTED - MoveIt
MoveGroup->GoalStatus = ABORTED(withMoveItErrorCodein the result) - ros2_control controller actions -> aborted goals
Neither the diagnostic bridge nor the log bridge sees this. This bridge does, generically, by observing the goal-status topic.
What it does
- Discovers every action on the graph by scanning for
*/_action/statustopics (re-scanned on a timer to catch actions that appear later, to drop actions that vanish, e.g. a Nav2 lifecycle deactivate or a one-shot node, and to retry any fault whose delivery to the FaultManager was deferred). -
ABORTED (6)-> fault (SEVERITY_ERRORby default). -
CANCELED (5)-> fault only ifcanceled_is_fault(off by default; cancel is usually intentional). When enabled it emits a_CANCELEDcode. -
SUCCEEDED (4)->PASSEDto heal the action’s fault code (if enabled). -
fault_codeis<PREFIX>_<ACTION>_ABORTED(or_CANCELED), e.g.ACTION_NAVIGATE_TO_POSE_ABORTED.source_idis the action server’s node FQN (resolved from the status topic’s publisher, e.g./bt_navigator), so the fault associates with that node’s SOVD entity; it falls back to the action name only if no publisher is visible.
Per-action state, not per-goal
The fault is a property of the action, not of an individual goal. On every
status message the whole GoalStatusArray is scanned for the net state:
- if any goal is
ABORTED(orCANCELEDwhencanceled_is_fault), the action is failed (order of goals in the array does not matter); - otherwise, if the array has terminal goals and none are failing, the action is healthy.
A fault is raised only on the healthy -> failed transition and healed only on
failed -> healthy. This means one failed goal cannot heal an action while
another goal is still failed, and a dropped terminal message cannot leave a
fault stuck. Per-goal dedup (keyed on goal_id:status) only suppresses
duplicate log lines; it never gates the transition.
The bridge tracks the latest observed state (the desired state) separately from what the FaultManager was actually told. The transition is committed only once the report is delivered: if the FaultManager service is not discovered yet
- e.g. an action’s status topic is
transient_local(latched) and its terminal status reaches the bridge during the startup discovery window, before the report client connects - the transition stays pending and is retried, rather than the high-severity fault being silently dropped. (The latched status is delivered to a subscription only once, so without this retry the dropped report would never recover.) See Delivery latency and freeze-frame.
Scope: the terminal verdict, not the reason
This bridge delivers the generic “it aborted” event. The action-specific
reason (e.g. MoveItErrorCode.val = -26 START_STATE_IN_COLLISION, Nav2
error_code on Iron/Jazzy) lives in the action result message and is a separate
enrichment concern (a future action-result reader using runtime message
introspection / dynmsg). Per-project plugins are only needed for human-readable
labels, not to surface the fault.
Delivery latency and freeze-frame
The FaultManager captures the freeze-frame (topic snapshot, rosbag) when it receives the report, so the value of that context decays with delivery latency: a late fault is reported against state that has already moved on. The bridge therefore optimises for delivering a fault to the FaultManager as soon as physically possible.
- Happy path (service discovered): the fault is reported synchronously in the status callback, the instant the terminal status arrives. This is the normal runtime case and is already ASAP - nothing is deferred.
-
Degraded path (service not yet discovered): a fault cannot physically be
delivered before the FaultManager
report_faultservice is discovered (startup before the FaultManager is up, or a FaultManager restart). The transition is kept pending and retried on a dedicated fast retry timer (retry_period_sec, default 50 ms), decoupled from the slow discoveryrescan. So the fault lands within one short tick of the service appearing - keeping the freeze-frame as contemporaneous as the discovery floor allows.
We deliberately do not block the (single-threaded) executor waiting for the service: blocking would stall detection of other actions’ faults during an outage to shave the last few milliseconds off one fault - a bad trade when the freeze-frame is already only ~tens of ms fresher. The retry timer is armed only while a delivery is pending, so an idle bridge does no periodic work.
Known boundaries (narrow, and shrunk further by the fast retry; both are consequences of the level-triggered, net-state model, not silent loss in the common case):
File truncated at 100 lines see the full file
Changelog for package ros2_medkit_action_status_bridge
0.6.0 (2026-06-22)
- Initial release: generic action-status bridge. Watches every
/<action>/_action/statustopic and turns ABORTED goals into FaultManager faults (<PREFIX>_<ACTION>_ABORTED), heals on a non-failing terminal state. Captures the terminal action verdict that the diagnostic and log bridges cannot see (#423). - Fault state is per-ACTION, derived from the whole
GoalStatusArrayon each message: a fault is raised only on thehealthy -> failedtransition and healed only onfailed -> healthy, so it is order-independent and resilient to dropped terminal messages. Per-goal dedup now only suppresses duplicate log lines. -
canceled_is_faultemits a status-aware<PREFIX>_<ACTION>_CANCELEDcode; such a fault heals on the next non-failing terminal or when the canceled goal ages out of the retained status array. - Vanished actions (lifecycle deactivate, one-shot nodes) are pruned on rescan.
- Parameters are range-checked/normalized at load; an incompatible action status QoS is now warned about instead of silently dropping faults.
- Ships a default configuration so the bridge starts out of the box (#459)
- Fault source is fixed at first report instead of being re-attributed later: faults raised before discovery settles register the server FQN, faults are never attributed to the discovery placeholder node name, and the subscriptions and timer are reset in the destructor (#466)
- Contributors: \@bburda, \@mfaferek93
Package Dependencies
System Dependencies
Dependant Packages
| Name | Deps |
|---|---|
| ros2_medkit_gateway |