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>
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:
- The
OnAcknowledgecallback fires with(context, condition, eventId, comment) - Look up the alarm's
AckMsgtag reference fromAlarmInfo - Write the comment text (or empty string if no comment) to
[AlarmTag].AckMsgvia_mxAccessClient.WriteAsync - Return
Good— the actualAckedStateupdate happens when Galaxy setsAcked=trueand we receive the data change
2. Galaxy Acked Data Change → OPC UA AckedState Update
When [AlarmTag].Acked changes in the Galaxy runtime:
- The auto-subscription delivers a data change to the dispatch loop
- Detect
Ackedtag transitions (same pattern as InAlarm detection) - Update
condition.SetAcknowledgedState(SystemContext, true/false)on theAlarmConditionState - Update
condition.Retain.Value(retain while active or unacknowledged) - 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 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
- Build clean, all tests pass
- Deploy service
alarmsCLI → subscribe to TestMachine_001- Trigger alarm (write TestAlarm001=true) → event shows
Unacknowledged - In System Platform, acknowledge the alarm → CLI shows updated event with
Acknowledged - Or use OPC UA client to call Acknowledge method on the condition node → Galaxy AckMsg is written