diff --git a/docs/AlarmTracking.md b/docs/AlarmTracking.md index 70b0f1f..5f19896 100644 --- a/docs/AlarmTracking.md +++ b/docs/AlarmTracking.md @@ -67,19 +67,16 @@ After alarm condition nodes are created, `SubscribeAlarmTags` opens MXAccess sub These subscriptions are opened unconditionally (not ref-counted) because they serve the server's own alarm tracking, not client-initiated monitoring. Tags that do not have corresponding variable nodes in `_tagToVariableNode` are skipped. -## EventNotifier on Parent Nodes +## EventNotifier Propagation -When a Galaxy object contains at least one alarm attribute, its OPC UA node is updated to include `EventNotifiers.SubscribeToEvents`: +When a Galaxy object contains at least one alarm attribute, `EventNotifiers.SubscribeToEvents` is set on the object node **and all its ancestors** up to the root. This allows OPC UA clients to subscribe to events at any level in the hierarchy and receive alarm notifications from all descendants: ```csharp if (hasAlarms && _nodeMap.TryGetValue(obj.GobjectId, out var objNode)) -{ - if (objNode is BaseObjectState objState) - objState.EventNotifier = EventNotifiers.SubscribeToEvents; -} + EnableEventNotifierUpChain(objNode); ``` -This allows OPC UA clients to subscribe to events on the parent object node and receive alarm notifications for all child attributes. The root `ZB` folder also has `EventNotifiers.SubscribeToEvents` enabled during initial construction. +For example, an alarm on `TestMachine_001.SubObject.Temperature` will be visible to clients subscribed on `SubObject`, `TestMachine_001`, or the root `ZB` folder. The root `ZB` folder also has `EventNotifiers.SubscribeToEvents` enabled during initial construction. ## InAlarm Transition Detection in DispatchLoop @@ -125,9 +122,7 @@ Key behaviors: - **Retain** -- `true` while the alarm is active or unacknowledged. This keeps the condition visible in condition refresh responses. - **Acknowledged state** -- Reset to `false` when the alarm activates, requiring explicit client acknowledgment. -The event is reported through two paths: -1. **Parent node** -- `sourceVar.Parent.ReportEvent` propagates the event to clients subscribed on the parent Galaxy object. -2. **Server node** -- `Server.ReportEvent` ensures clients subscribed at the server level also receive the event. +The event is reported by walking up the notifier chain from the source variable's parent through all ancestor nodes. Each ancestor with `EventNotifier` set receives the event via `ReportEvent`, so clients subscribed at any level in the Galaxy hierarchy see alarm transitions from descendant objects. ## Condition Refresh Override diff --git a/service_info.md b/service_info.md index b6400b2..20912b1 100644 --- a/service_info.md +++ b/service_info.md @@ -117,6 +117,32 @@ alarmack write → denied (BadUserAccessDenied) bad password → denied (connection rejected) ``` +## Alarm Notifier Chain Update + +Updated: `2026-03-28` + +Both instances updated with alarm event propagation up the notifier chain. + +Code changes: +- Alarm events now walk up the parent chain (`ReportEventUpNotifierChain`), reporting to every ancestor node +- `EventNotifier = SubscribeToEvents` is set on all ancestors of alarm-containing nodes (`EnableEventNotifierUpChain`) +- Removed separate `Server.ReportEvent` call (no longer needed — the walk reaches the root) + +No configuration changes required — alarm tracking was already enabled (`AlarmTrackingEnabled: true`). + +Verification (instance1, port 4840): +``` +alarms --node TestArea --refresh: + TestMachine_001.TestAlarm001 → visible (Severity=500, Retain=True) + TestMachine_001.TestAlarm002 → visible (Severity=500, Retain=True) + TestMachine_001.TestAlarm003 → visible (Severity=500, Retain=True) + TestMachine_002.TestAlarm001 → visible (Severity=500, Retain=True) + TestMachine_002.TestAlarm003 → visible (Severity=500, Retain=True) + +alarms --node DEV --refresh: + Same 5 alarms visible at DEV (grandparent) level +``` + ## Notes The service deployment and restart succeeded. The live CLI checks confirm the endpoint is reachable and that the array node identifier has changed to the bracketless form. The array value on the live service still prints as blank even though the status is good, so if this environment should have populated `MoveInPartNumbers`, the runtime data path still needs follow-up investigation. diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxNodeManager.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxNodeManager.cs index 9fee836..a0b5479 100644 --- a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxNodeManager.cs +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxNodeManager.cs @@ -423,14 +423,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa hasAlarms = true; } - // Enable EventNotifier on object nodes that contain alarms + // Enable EventNotifier on this node and all ancestors so alarm events propagate if (hasAlarms && _nodeMap.TryGetValue(obj.GobjectId, out var objNode)) - { - if (objNode is BaseObjectState objState) - objState.EventNotifier = EventNotifiers.SubscribeToEvents; - else if (objNode is FolderState folderState) - folderState.EventNotifier = EventNotifiers.SubscribeToEvents; - } + EnableEventNotifierUpChain(objNode); } // Auto-subscribe to InAlarm tags so we detect alarm transitions @@ -519,14 +514,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa if (active) condition.SetAcknowledgedState(SystemContext, false); - // Report through the source node hierarchy so events reach subscribers on parent objects - if (_tagToVariableNode.TryGetValue(info.SourceTagReference, out var sourceVar) && sourceVar.Parent != null) - { - sourceVar.Parent.ReportEvent(SystemContext, condition); - } - - // Also report to Server node for clients subscribed at server level - Server.ReportEvent(SystemContext, condition); + // Walk up the notifier chain so events reach subscribers at any ancestor level + if (_tagToVariableNode.TryGetValue(info.SourceTagReference, out var sourceVar)) + ReportEventUpNotifierChain(sourceVar, condition); Log.Information("Alarm {State}: {Source} (Severity={Severity}, Message={Message})", active ? "ACTIVE" : "CLEARED", info.SourceName, severity, message); @@ -879,12 +869,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa } if (hasAlarms && _nodeMap.TryGetValue(obj.GobjectId, out var objNode)) - { - if (objNode is BaseObjectState objState) - objState.EventNotifier = EventNotifiers.SubscribeToEvents; - else if (objNode is FolderState folderState) - folderState.EventNotifier = EventNotifiers.SubscribeToEvents; - } + EnableEventNotifierUpChain(objNode); } // Subscribe alarm tags for new subtree @@ -1192,6 +1177,23 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa return roles != null && roles.Contains(requiredRole); } + private static void EnableEventNotifierUpChain(NodeState node) + { + for (var current = node as BaseInstanceState; current != null; current = current.Parent as BaseInstanceState) + { + if (current is BaseObjectState obj) + obj.EventNotifier = EventNotifiers.SubscribeToEvents; + else if (current is FolderState folder) + folder.EventNotifier = EventNotifiers.SubscribeToEvents; + } + } + + private void ReportEventUpNotifierChain(BaseInstanceState sourceNode, IFilterTarget eventInstance) + { + for (var current = sourceNode.Parent; current != null; current = (current as BaseInstanceState)?.Parent) + current.ReportEvent(SystemContext, eventInstance); + } + private bool TryApplyArrayElementWrite(string tagRef, object? writeValue, string indexRange, out object updatedArray) { updatedArray = null!; @@ -1761,9 +1763,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa condition.SetAcknowledgedState(SystemContext, acked); condition.Retain.Value = (condition.ActiveState?.Id?.Value == true) || !acked; - if (_tagToVariableNode.TryGetValue(info.SourceTagReference, out var src) && src.Parent != null) - src.Parent.ReportEvent(SystemContext, condition); - Server.ReportEvent(SystemContext, condition); + if (_tagToVariableNode.TryGetValue(info.SourceTagReference, out var src)) + ReportEventUpNotifierChain(src, condition); Log.Information("Alarm {AckState}: {Source}", acked ? "ACKNOWLEDGED" : "UNACKNOWLEDGED", info.SourceName);