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

6.0 KiB

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

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:

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:

condition.OnAcknowledgeCalled = OnAlarmAcknowledge;

New: OnAlarmAcknowledge callback method

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:

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:

if (_alarmAckedTags.TryGetValue(address, out var ackedAlarmInfo))
{
    var newAcked = IsTrue(vtq.Value);
    pendingAckedEvents.Add((ackedAlarmInfo, newAcked));
}

Inside the Lock block, apply acked state changes:

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 testAlarmAcknowledgeTests.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