Add ServerCapabilities/OperationLimits node, enable diagnostics, add OnModifyMonitoredItemsComplete override for DA compliance. Wire shelving, enable/disable, confirm, and addcomment handlers on alarm conditions with LocalTime/Quality event fields for Part 9 compliance. Add Aes128/Aes256 security profiles, X.509 certificate authentication, and AUDIT-prefixed auth logging. Fix flaky probe monitor test. Update docs for all changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
171 lines
10 KiB
Markdown
171 lines
10 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.
|
|
|
|
### Condition Methods
|
|
|
|
Each alarm condition supports the following OPC UA Part 9 methods:
|
|
|
|
- **Acknowledge** (`OnAcknowledge`) -- Writes the acknowledgment message to the Galaxy `AckMsg` tag. Requires the `AlarmAck` role.
|
|
- **Confirm** (`OnConfirm`) -- Confirms a previously acknowledged alarm. The SDK manages the `ConfirmedState` transition.
|
|
- **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 the `EnabledState` transition.
|
|
- **Shelve** (`OnShelve`) -- Supports `TimedShelve`, `OneShotShelve`, and `Unshelve` operations. The SDK manages the `ShelvedStateMachineType` state 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/Confirm
|
|
- `ActiveState`, `AckedState`, `ConfirmedState` -- State transitions
|
|
- `Message` -- Alarm message from Galaxy `DescAttrName` or default text
|
|
- `Severity` -- Galaxy Priority clamped to OPC UA range 1-1000
|
|
- `Retain` -- True while alarm is active or unacknowledged
|
|
- `LocalTime` -- Server timezone offset with daylight saving flag
|
|
- `Quality` -- 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:
|
|
|
|
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 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:
|
|
|
|
```csharp
|
|
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`:
|
|
|
|
```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. When role-based auth is active, alarm acknowledgment requires the `AlarmAck` role on the session (checked via `GrantedRoleIds`). Users without this role receive `BadUserAccessDenied`.
|
|
|
|
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:
|
|
|
|
```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.
|