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>
162 lines
6.0 KiB
Markdown
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
|