Skip to content

fix(opcua): drop non-condition events from the alarm path - #552

Merged
mfaferek93 merged 6 commits into
mainfrom
fix/opcua-drop-non-condition-events
Jul 24, 2026
Merged

fix(opcua): drop non-condition events from the alarm path#552
mfaferek93 merged 6 commits into
mainfrom
fix/opcua-drop-non-condition-events

Conversation

@mfaferek93

@mfaferek93 mfaferek93 commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

The server-wide EventNotifier (ns=0;i=2253, the Server object) emits BaseEventType/AuditEventType housekeeping events - session-state changes, audit/system events (e.g. "Session state changed to Created") - alongside genuine A&C Condition events. The catch-all alarm mapping routed all of them to a single PLC_ALARM fault code, and because the fault store dedups by code, a housekeeping event could overwrite and mask a real process alarm sharing that code.

Only ConditionType-derived events carry a non-null ConditionId (the Part 9 auto-prepended select clause). Guarding on_event on a non-null ConditionId drops the non-condition housekeeping events cleanly while leaving the real-alarm path untouched - genuine conditions always carry a non-null ConditionId (the plugin already keys per-condition tracking and Ack/Confirm on it).

Verified: colcon test ros2_medkit_opcua green - 265 tests including the live open62541 A&C integration tests that drive real AlarmConditionType events (real conditions still flow through).

The server-wide EventNotifier also emits BaseEventType/AuditEventType
housekeeping (session-state, audit) with a null ConditionId. Routing
those through the catch-all alarm mapping produced a PLC_ALARM that
masked a real process alarm, since the fault store dedups by code.
Guard on a non-null ConditionId so only genuine A&C conditions raise.
Copilot AI review requested due to automatic review settings July 23, 2026 14:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Prevents OPC UA server-wide EventNotifier “housekeeping” events (BaseEventType/AuditEventType) from entering the alarm/condition processing path in ros2_medkit_opcua, avoiding PLC_ALARM fault-code dedup collisions that can mask real process alarms.

Changes:

  • Add an early-return guard in OpcuaPoller::on_event() to ignore events with a null ConditionId.
  • Emit a debug log when such non-condition events are dropped.

Comment thread src/ros2_medkit_plugins/ros2_medkit_opcua/src/opcua_poller.cpp Outdated
The drop-non-condition guard used a raw condition_id.isNull(); switch it
to the shared is_condition_event() helper, which has focused unit tests
(NullConditionIdIsRejected / RealConditionIdIsAccepted) asserting the
exact drop-null / keep-real decision. Remove the now-dead duplicate of
the same check in the auto-alarm branch - the hoisted guard covers it.
@mfaferek93 mfaferek93 self-assigned this Jul 23, 2026

@bburda bburda left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional findings outside the diff:

  • README.md still describes this drop as auto_alarms only. The paragraph starting "System messages:" is inside the auto_alarms chapter and says auto_alarms drops those events before deriving a fault. After this change it applies to every alarm source. The same paragraph says to use exclude for the rest, but auto_alarm_passes_filters runs only in the auto branch, so an explicit event_alarms entry has no text filter at all.
  • README.md also says "An event matching no mapping uses the source-level fault_code; if that is also empty the event is ignored." This is what the README promises for the catch-all ns=0;i=2253 example with fault_code: PLC_GENERIC_ALARM. Non-Condition events now never reach NodeMap::resolve_alarm, so this sentence needs the exception.
  • design/index.rst lists the decision order without the ConditionId condition. It is now a global rule that runs before that table.
  • The doc comment on is_condition_event in opcua_poller.hpp and the comment above IsConditionEventTest still say the filter is for auto_alarms.
  • Test coverage: IsConditionEventTest only checks the predicate. It passes the same way before and after the move, so it does not cover the routing this PR changes. The live fixture can emit Conditions only: every trigger goes through UA_Server_triggerConditionEvent on nodes built by UA_Server_createCondition, and there is no command for a plain event. To cover this, add a fixture command that fires a BaseEventType on i=2253 with UA_Server_triggerEvent. Then add an integration case with an explicit event_alarms entry on that source: the plain event must produce no fault, and a real condition on the same source must still produce its fault. The second check matters too, it catches dropping more than intended.
  • Pre-existing, not caused by this PR: reconcile_after_refresh does not filter by source, while reconcile_after_read filters on runtime.source_id. In open62541 ConditionRefresh loops over every monitored item in the subscription and sends a separate RefreshStart/RefreshEnd pair for each one. The plugin creates one monitored item per alarm source on a single subscription. With two sources the first RefreshEnd reconciles against a refresh_seen_ set that holds only the conditions of the first item. Conditions from the other source are then cleared and raised again by the next burst. Worth a separate issue.

Comment thread src/ros2_medkit_plugins/ros2_medkit_opcua/src/opcua_poller.cpp Outdated
Comment thread src/ros2_medkit_plugins/ros2_medkit_opcua/src/opcua_poller.cpp
The old comment claimed an unfiltered non-condition event maps to its
fault_code and masks a genuine alarm. It does not: every alarm-state
select clause resolves to null, so the state machine returns ReportHealed
(a no-op) and no fault is reported. The real harm is the latched
null-ConditionId entry apply_condition_state would leave in conditions_,
which drives a spurious ClearFault on the next ConditionRefresh
(should_clear_after_refresh treats Healed as active) and can misroute a
later Acknowledge/Confirm via lookup_condition. Comment only.
…licit source

The non-condition guard drops with a DEBUG line only, so on an explicit
event_alarms source a real alarm arriving with a null ConditionId (edge
open62541 servers, or an AlarmConditionType fired via triggerEvent) was
silenced with no operator signal - a regression vs the pre-PR explicit
path. Add a per-source warn-once (mirrors read_unsupported_warned_sources_)
gated on the source having a fault_code or mappings, so the synthetic auto
source keeps the quiet DEBUG path.
The guard now runs for every alarm source, not just auto_alarms. Update
the README (mapping precedence exception + System messages section), the
design decision table (new ConditionId rule 0), and the is_condition_event
doc + IsConditionEventTest comment to match.

Add a routing regression: a 'sysevent' fixture command fires a BaseEventType
on i=2253 via UA_Server_triggerEvent (null ConditionId), and an integration
case with an explicit event_alarms catch-all on ns=0;i=2253 -> PLC_ALARM
asserts the sysevent never surfaces as PLC_ALARM while a real condition on
the catch-all still faults.
@mfaferek93
mfaferek93 merged commit 7939c94 into main Jul 24, 2026
24 of 26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants