14 KiB
Alarm Tracking
LmxNodeManager generates OPC UA alarm conditions from Galaxy attributes marked as alarms. The system detects alarm-capable attributes during address space construction, creates AlarmConditionState nodes, auto-subscribes to the runtime alarm tags via MXAccess, and reports state transitions as OPC UA events.
AlarmInfo Structure
Each tracked alarm is represented by an AlarmInfo instance stored in the _alarmInAlarmTags dictionary, keyed by the InAlarm tag reference:
private sealed class AlarmInfo
{
public string SourceTagReference { get; set; } // e.g., "Tag_001.Temperature"
public NodeId SourceNodeId { get; set; }
public string SourceName { get; set; } // attribute name for event messages
public bool LastInAlarm { get; set; } // tracks previous state for edge detection
public AlarmConditionState? ConditionNode { get; set; }
public string PriorityTagReference { get; set; } // e.g., "Tag_001.Temperature.Priority"
public string DescAttrNameTagReference { get; set; } // e.g., "Tag_001.Temperature.DescAttrName"
public ushort CachedSeverity { get; set; }
public string CachedMessage { get; set; }
}
LastInAlarm enables edge detection so only actual transitions (inactive-to-active or active-to-inactive) generate events, not repeated identical values.
Alarm Detection via is_alarm Flag
During BuildAddressSpace (and BuildSubtree for incremental sync), the node manager scans each non-area Galaxy object for attributes where IsAlarm == true and PrimitiveName is empty (direct attributes only, not primitive children):
var alarmAttrs = objAttrs.Where(a => a.IsAlarm && string.IsNullOrEmpty(a.PrimitiveName)).ToList();
The IsAlarm flag originates from the AlarmExtension primitive in the Galaxy repository database. When a Galaxy attribute has an associated AlarmExtension primitive, the SQL query sets is_alarm = 1 on the corresponding GalaxyAttributeInfo.
For each alarm attribute, the code verifies that a corresponding InAlarm sub-attribute variable node exists in _tagToVariableNode (constructed from FullTagReference + ".InAlarm"). If the variable node is missing, the alarm is skipped -- this prevents creating orphaned alarm conditions for attributes whose extension primitives were not published.
Template-Based Alarm Object Filter
When large galaxies contain more alarm-bearing objects than clients need, OpcUa.AlarmFilter.ObjectFilters restricts alarm condition creation to a subset of objects selected by template name pattern. The filter is applied at both alarm creation sites -- the full build in BuildAddressSpace and the subtree rebuild path triggered by Galaxy redeployment -- so the included set is recomputed on every rebuild against the fresh hierarchy.
Matching rules
*is the only wildcard (glob-style, zero or more characters). All other regex metacharacters are escaped and matched literally.- Matching is case-insensitive.
- The leading
$used by Galaxy templatetag_namevalues is normalized away on both the stored chain entry and the operator pattern, soTestMachine*matches the stored$TestMachine. - Each configured entry may itself be comma-separated for operator convenience (
"TestMachine*, Pump_*"). - An empty list disables the filter and restores the prior behavior: every alarm-bearing object is tracked when
AlarmTrackingEnabled=true.
What gets included
Every Galaxy object whose template derivation chain contains any template matching any pattern is included. The chain walks gobject.derived_from_gobject_id from the instance through its immediate template and each ancestor template, up to $Object. An instance of TestCoolMachine whose chain is $TestCoolMachine -> $TestMachine -> $UserDefined matches the pattern TestMachine via the ancestor hit.
Inclusion propagates down the containment hierarchy: if an object matches, all of its descendants are included as well, regardless of their own template chains. This lets operators target a parent and pick up all its alarm-bearing children with one pattern.
Each object is evaluated exactly once. Overlapping matches (multiple patterns hit, or both an ancestor and descendant match independently) never produce duplicate alarm condition subscriptions -- the filter operates on object identity via a HashSet<int> of included GobjectId values.
Resolution algorithm
AlarmObjectFilter.ResolveIncludedObjects(hierarchy) runs once per build:
- Compile each pattern into a regex with
IgnoreCase | CultureInvariant | Compiled. - Build a
parent -> childrenmap from the hierarchy. Orphans (parent id not in the hierarchy) are treated as roots. - BFS from each root with a
(nodeId, parentIncluded)queue and avisitedset for cycle defense. - At each node: if the parent was included OR any chain entry matches any pattern, add the node and mark its subtree as included.
- Return the
HashSet<int>of included object IDs. When no patterns are configured the filter is disabled and the method returnsnull, which the alarm loop treats as "no filtering".
After each resolution, UnmatchedPatterns exposes any raw pattern that matched zero objects so the startup log can warn about operator typos without failing startup.
How the alarm loop applies the filter
// LmxNodeManager.BuildAddressSpace (and the subtree rebuild path)
if (_alarmTrackingEnabled)
{
var includedIds = ResolveAlarmFilterIncludedIds(sorted); // null if no filter
foreach (var obj in sorted)
{
if (obj.IsArea) continue;
if (includedIds != null && !includedIds.Contains(obj.GobjectId)) continue;
// ... existing alarm-attribute collection + AlarmConditionState creation
}
}
ResolveAlarmFilterIncludedIds also emits a one-line summary (Alarm filter: X of Y objects included (Z pattern(s))) and per-pattern warnings for patterns that matched nothing. The included count is published to the dashboard via AlarmFilterIncludedObjectCount.
Runtime telemetry
LmxNodeManager exposes three read-only properties populated by the filter:
AlarmFilterEnabled-- true when patterns are configured.AlarmFilterPatternCount-- number of compiled patterns.AlarmFilterIncludedObjectCount-- number of objects in the most recent included set.
StatusReportService reads these into AlarmStatusInfo.FilterEnabled, FilterPatternCount, and FilterIncludedObjectCount. The Alarms panel on the dashboard renders Filter: N pattern(s), M object(s) included only when the filter is enabled. See Status Dashboard.
Validator warning
ConfigurationValidator.ValidateAndLog() logs the effective filter at startup and emits a Warning if AlarmFilter.ObjectFilters is non-empty while AlarmTrackingEnabled is false, because the filter would have no effect.
AlarmConditionState Creation
Each detected alarm attribute produces an AlarmConditionState node:
var condition = new AlarmConditionState(sourceVariable);
condition.Create(SystemContext, conditionNodeId,
new QualifiedName(alarmAttr.AttributeName + "Alarm", NamespaceIndex),
new LocalizedText("en", alarmAttr.AttributeName + " Alarm"),
true);
Key configuration on the condition node:
- SourceNode -- Set to the OPC UA NodeId of the source variable, linking the condition to the attribute that triggered it.
- SourceName / ConditionName -- Set to the Galaxy attribute name for identification in event notifications.
- AutoReportStateChanges -- Set to
trueso the OPC UA framework automatically generates event notifications when condition properties change. - Initial state -- Enabled, inactive, acknowledged, severity Medium, retain false.
- HasCondition references -- Bidirectional references are added between the source variable and the condition node.
The condition's OnReportEvent callback forwards events to Server.ReportEvent so they reach clients subscribed at the server level.
Condition Methods
Each alarm condition supports the following OPC UA Part 9 methods:
- Acknowledge (
OnAcknowledge) -- Writes the acknowledgment message to the GalaxyAckMsgtag. Requires theAlarmAckrole. - Confirm (
OnConfirm) -- Confirms a previously acknowledged alarm. The SDK manages theConfirmedStatetransition. - AddComment (
OnAddComment) -- Attaches an operator comment to the condition for audit trail purposes. - Enable / Disable (
OnEnableDisable) -- Activates or deactivates alarm monitoring for the specific condition. The SDK manages theEnabledStatetransition. - Shelve (
OnShelve) -- SupportsTimedShelve,OneShotShelve, andUnshelveoperations. The SDK manages theShelvedStateMachineTypestate transitions including automatic timed unshelve. - TimedUnshelve (
OnTimedUnshelve) -- Automatically called by the SDK when a timed shelve period expires.
Event Fields
Alarm events include the following fields:
EventId-- Unique GUID for each event, used as reference for Acknowledge/ConfirmActiveState,AckedState,ConfirmedState-- State transitionsMessage-- Alarm message from GalaxyDescAttrNameor default textSeverity-- Galaxy Priority clamped to OPC UA range 1-1000Retain-- True while alarm is active or unacknowledgedLocalTime-- Server timezone offset with daylight saving flagQuality-- Set to Good for alarm events
Auto-subscription to Alarm Tags
After alarm condition nodes are created, SubscribeAlarmTags opens MXAccess subscriptions for three tags per alarm:
- InAlarm (
Tag_001.Temperature.InAlarm) -- The boolean trigger for alarm activation/deactivation. - Priority (
Tag_001.Temperature.Priority) -- Numeric priority that maps to OPC UA severity. - DescAttrName (
Tag_001.Temperature.DescAttrName) -- String description used as the alarm event message.
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 Propagation
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:
if (hasAlarms && _nodeMap.TryGetValue(obj.GobjectId, out var objNode))
EnableEventNotifierUpChain(objNode);
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
Alarm state changes are detected in the dispatch loop's Phase 1 (outside Lock), which runs on the background dispatch thread rather than the STA thread. This placement is intentional because the detection logic reads Priority and DescAttrName values from MXAccess, which would block the STA thread if done inside the OnMxAccessDataChange callback.
For each pending data change, the loop checks whether the address matches a key in _alarmInAlarmTags:
if (_alarmInAlarmTags.TryGetValue(address, out var alarmInfo))
{
var newInAlarm = vtq.Value is true || vtq.Value is 1
|| (vtq.Value is int intVal && intVal != 0);
if (newInAlarm != alarmInfo.LastInAlarm)
{
alarmInfo.LastInAlarm = newInAlarm;
// Read Priority and DescAttrName via MXAccess (outside Lock)
...
pendingAlarmEvents.Add((alarmInfo, newInAlarm));
}
}
The boolean coercion handles multiple value representations: true, integer 1, or any non-zero integer. When the value changes state, Priority and DescAttrName are read synchronously from MXAccess to populate CachedSeverity and CachedMessage. These reads happen outside Lock because they call into the STA thread.
Priority values are clamped to the OPC UA severity range (1-1000). Both int and short types are handled.
ReportAlarmEvent
ReportAlarmEvent runs inside Lock during Phase 2 of the dispatch loop. It updates the AlarmConditionState and generates an OPC UA event:
condition.SetActiveState(SystemContext, active);
condition.Message.Value = new LocalizedText("en", message);
condition.SetSeverity(SystemContext, (EventSeverity)severity);
condition.Retain.Value = active || (condition.AckedState?.Id?.Value == false);
Key behaviors:
- Active state -- Set to
trueon activation,falseon clearing. - Message -- Uses
CachedMessage(from DescAttrName) when available on activation. Falls back to a generated"Alarm active: {SourceName}"string. Cleared alarms always use"Alarm cleared: {SourceName}". - Severity -- Set from
CachedSeverity, which was read from the Priority tag. - Retain --
truewhile the alarm is active or unacknowledged. This keeps the condition visible in condition refresh responses. - Acknowledged state -- Reset to
falsewhen the alarm activates, requiring explicit client acknowledgment. When role-based auth is active, alarm acknowledgment requires theAlarmAckrole on the session (checked viaGrantedRoleIds). Users without this role receiveBadUserAccessDenied.
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
The ConditionRefresh override iterates all tracked alarms and queues retained conditions to the requesting monitored items:
public override ServiceResult ConditionRefresh(OperationContext context,
IList<IEventMonitoredItem> monitoredItems)
{
foreach (var kvp in _alarmInAlarmTags)
{
var info = kvp.Value;
if (info.ConditionNode == null || info.ConditionNode.Retain?.Value != true)
continue;
foreach (var item in monitoredItems)
item.QueueEvent(info.ConditionNode);
}
return ServiceResult.Good;
}
Only conditions where Retain.Value == true are included. This means only active or unacknowledged alarms appear in condition refresh responses, matching the OPC UA specification requirement that condition refresh returns the current state of all retained conditions.