Provides technical documentation covering OPC UA server, address space, Galaxy repository, MXAccess bridge, data types, read/write, subscriptions, alarms, historian, incremental sync, configuration, dashboard, service hosting, and CLI tool. Updates README with component documentation table. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
153 lines
8.2 KiB
Markdown
153 lines
8.2 KiB
Markdown
# 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:
|
|
|
|
```csharp
|
|
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):
|
|
|
|
```csharp
|
|
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.
|
|
|
|
## AlarmConditionState Creation
|
|
|
|
Each detected alarm attribute produces an `AlarmConditionState` node:
|
|
|
|
```csharp
|
|
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 `true` so 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.
|
|
|
|
## Auto-subscription to Alarm Tags
|
|
|
|
After alarm condition nodes are created, `SubscribeAlarmTags` opens MXAccess subscriptions for three tags per alarm:
|
|
|
|
1. **InAlarm** (`Tag_001.Temperature.InAlarm`) -- The boolean trigger for alarm activation/deactivation.
|
|
2. **Priority** (`Tag_001.Temperature.Priority`) -- Numeric priority that maps to OPC UA severity.
|
|
3. **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 on Parent Nodes
|
|
|
|
When a Galaxy object contains at least one alarm attribute, its OPC UA node is updated to include `EventNotifiers.SubscribeToEvents`:
|
|
|
|
```csharp
|
|
if (hasAlarms && _nodeMap.TryGetValue(obj.GobjectId, out var objNode))
|
|
{
|
|
if (objNode is BaseObjectState objState)
|
|
objState.EventNotifier = EventNotifiers.SubscribeToEvents;
|
|
}
|
|
```
|
|
|
|
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.
|
|
|
|
## 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`:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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 `true` on activation, `false` on 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** -- `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.
|
|
|
|
## Condition Refresh Override
|
|
|
|
The `ConditionRefresh` override iterates all tracked alarms and queues retained conditions to the requesting monitored items:
|
|
|
|
```csharp
|
|
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.
|