Files
lmxopcua/alarm_ack.md
Joseph Doherty 9368767b1b Add alarm acknowledge plan and incorporate code review fixes
Adds alarm_ack.md documenting the two-way acknowledge flow (OPC UA client
writes AckMsg, Galaxy confirms via Acked data change). Includes external
code review fixes for subscriptions and node manager, and removes stale
plan files now superseded by component documentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:02:47 -04:00

162 lines
6.0 KiB
Markdown

# Alarm Acknowledge Plan
## Context
The server creates `AlarmConditionState` nodes for Galaxy alarms and monitors `[AlarmTag].InAlarm` for activation. Currently there is no acknowledge support — the `AckedState` is set to false when an alarm activates but there is no way for an OPC UA client to acknowledge it.
Galaxy alarm acknowledgment works by writing to `[AlarmTag].AckMsg`. Writing any string (including empty) to `AckMsg` triggers the acknowledge in System Platform. Once acknowledged, the runtime sets `[AlarmTag].Acked` to `true`.
## Design
Two parts:
### 1. OPC UA Acknowledge → Galaxy AckMsg Write
When an OPC UA client calls the Acknowledge method on an `AlarmConditionState` node:
1. The `OnAcknowledge` callback fires with `(context, condition, eventId, comment)`
2. Look up the alarm's `AckMsg` tag reference from `AlarmInfo`
3. Write the comment text (or empty string if no comment) to `[AlarmTag].AckMsg` via `_mxAccessClient.WriteAsync`
4. Return `Good` — the actual `AckedState` update happens when Galaxy sets `Acked=true` and we receive the data change
### 2. Galaxy Acked Data Change → OPC UA AckedState Update
When `[AlarmTag].Acked` changes in the Galaxy runtime:
1. The auto-subscription delivers a data change to the dispatch loop
2. Detect `Acked` tag transitions (same pattern as InAlarm detection)
3. Update `condition.SetAcknowledgedState(SystemContext, true/false)` on the `AlarmConditionState`
4. Update `condition.Retain.Value` (retain while active or unacknowledged)
5. Report the state change event
## Changes
### `AlarmInfo` class — add tag references
```csharp
public string AckedTagReference { get; set; } = "";
public string AckMsgTagReference { get; set; } = "";
```
### `BuildAddressSpace` alarm tracking — populate new fields and wire OnAcknowledge
In the alarm creation block, after setting up the `AlarmConditionState`:
```csharp
var baseTagRef = alarmAttr.FullTagReference.TrimEnd('[', ']');
// existing:
PriorityTagReference = baseTagRef + ".Priority",
DescAttrNameTagReference = baseTagRef + ".DescAttrName",
// new:
AckedTagReference = baseTagRef + ".Acked",
AckMsgTagReference = baseTagRef + ".AckMsg",
```
Wire the acknowledge callback on the condition node:
```csharp
condition.OnAcknowledgeCalled = OnAlarmAcknowledge;
```
### New: `OnAlarmAcknowledge` callback method
```csharp
private ServiceResult OnAlarmAcknowledge(
ISystemContext context, ConditionState condition, byte[] eventId, LocalizedText comment)
{
// Find the AlarmInfo for this condition
var alarmInfo = _alarmInAlarmTags.Values
.FirstOrDefault(a => a.ConditionNode == condition);
if (alarmInfo == null)
return StatusCodes.BadNodeIdUnknown;
// Write the comment to AckMsg — writing any string (including empty) triggers ack in Galaxy
var ackMessage = comment?.Text ?? "";
_mxAccessClient.WriteAsync(alarmInfo.AckMsgTagReference, ackMessage)
.GetAwaiter().GetResult();
return ServiceResult.Good;
}
```
Don't update `AckedState` here — wait for the Galaxy to confirm via the `Acked` data change callback.
### `SubscribeAlarmTags` — subscribe to Acked tag
Add `AckedTagReference` to the list of tags subscribed per alarm (currently subscribes InAlarm, Priority, DescAttrName).
### New: `_alarmAckedTags` dictionary
Maps `[AlarmTag].Acked` tag reference → `AlarmInfo`, similar to `_alarmInAlarmTags` which maps InAlarm tags:
```csharp
private readonly Dictionary<string, AlarmInfo> _alarmAckedTags = new(...);
```
Populated alongside `_alarmInAlarmTags` during alarm tracking in `BuildAddressSpace`.
### `DispatchLoop` — detect Acked transitions
In the dispatch loop preparation phase (outside Lock), after the InAlarm check:
```csharp
if (_alarmAckedTags.TryGetValue(address, out var ackedAlarmInfo))
{
var newAcked = IsTrue(vtq.Value);
pendingAckedEvents.Add((ackedAlarmInfo, newAcked));
}
```
Inside the Lock block, apply acked state changes:
```csharp
foreach (var (info, acked) in pendingAckedEvents)
{
var condition = info.ConditionNode;
if (condition == null) continue;
condition.SetAcknowledgedState(SystemContext, acked);
condition.Retain.Value = (condition.ActiveState?.Id?.Value == true) || !acked;
// Report through parent and server
if (_tagToVariableNode.TryGetValue(info.SourceTagReference, out var src) && src.Parent != null)
src.Parent.ReportEvent(SystemContext, condition);
Server.ReportEvent(SystemContext, condition);
}
```
### `TearDownGobjects` — clean up `_alarmAckedTags`
Remove entries from `_alarmAckedTags` when tearing down gobjects (same as `_alarmInAlarmTags`).
### `BuildSubtree` — populate `_alarmAckedTags` for new subtree alarms
Same as the existing alarm tracking in `BuildSubtree` — add `_alarmAckedTags` population.
### Tests
**Unit test**`AlarmAcknowledgeTests.cs`:
- Create alarm attribute, trigger InAlarm=true, verify AckedState is false
- Simulate Acked=true data change, verify AckedState updates to true
- Verify Retain is false when alarm is inactive and acknowledged
**Integration test**:
- Create fixture with alarm attribute
- Push InAlarm=true → alarm fires, AckedState=false
- Write to AckMsg tag → verify WriteAsync called on MXAccess
- Push Acked=true → verify AckedState updates, Retain becomes false (if inactive)
## Files to Modify
| File | Change |
|---|---|
| `src/.../OpcUa/LmxNodeManager.cs` | Add `AckedTagReference`/`AckMsgTagReference` to AlarmInfo, add `_alarmAckedTags` dict, wire `OnAcknowledgeCalled`, add `OnAlarmAcknowledge` method, detect Acked transitions in DispatchLoop, subscribe to Acked tags, clean up in TearDown/BuildSubtree |
| `docs/AlarmTracking.md` | Update to document acknowledge flow |
## Verification
1. Build clean, all tests pass
2. Deploy service
3. `alarms` CLI → subscribe to TestMachine_001
4. Trigger alarm (write TestAlarm001=true) → event shows `Unacknowledged`
5. In System Platform, acknowledge the alarm → CLI shows updated event with `Acknowledged`
6. Or use OPC UA client to call Acknowledge method on the condition node → Galaxy AckMsg is written