# 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 _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