Propagate alarm events up the full notifier chain so subscribers at any ancestor see them
Previously alarms were only reported to the immediate parent node and the Server node. Now ReportEventUpNotifierChain walks the full parent chain so clients subscribed at TestArea see alarms from TestMachine_001, and EventNotifier is set on all ancestors of alarm-containing nodes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user