feat(server): route OPC UA Part 9 AddComment to ScriptedAlarmEngine
RouteScriptedAlarmMethodCalls now handles ConditionType.AddComment alongside Acknowledge/Confirm, dispatching to engine.AddCommentAsync. An empty comment is rejected by the Part 9 state machine and surfaced as BadInvalidArgument. MapCallOperation gates AddComment at the AlarmAcknowledge tier — there is no dedicated AddComment permission bit. Closes phase-7-status.md Gap 1: all Part 9 alarm methods now route to the engine. Adds 3 unit tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -652,8 +652,8 @@ public sealed class DriverNodeManager : CustomNodeManager2, IAddressSpaceBuilder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intercepts Part 9 Acknowledge / Confirm <see cref="CallMethodRequest"/> slots that
|
||||
/// target scripted alarm condition nodes and routes them to the
|
||||
/// Intercepts Part 9 Acknowledge / Confirm / AddComment <see cref="CallMethodRequest"/>
|
||||
/// slots that target scripted alarm condition nodes and routes them to the
|
||||
/// <see cref="ScriptedAlarmEngine"/>. Slots that are handled have their
|
||||
/// <paramref name="errors"/> entry set to <see cref="ServiceResult.Good"/> and are
|
||||
/// not touched by the caller's subsequent <c>base.Call</c> invocation.
|
||||
@@ -663,7 +663,9 @@ public sealed class DriverNodeManager : CustomNodeManager2, IAddressSpaceBuilder
|
||||
/// The OPC UA Part 9 Acknowledge method signature is:
|
||||
/// InputArguments[0] = EventId (ByteString, ignored — scripted alarms identify by
|
||||
/// ConditionId, not EventId), InputArguments[1] = Comment (LocalizedText).
|
||||
/// Confirm has the same shape. Missing or null comment is treated as empty string.
|
||||
/// Confirm and AddComment have the same two-argument shape. For Acknowledge /
|
||||
/// Confirm a missing comment is treated as empty; AddComment requires non-empty
|
||||
/// comment text and returns <see cref="StatusCodes.BadInvalidArgument"/> otherwise.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// User identity is extracted from <paramref name="userIdentity"/>'s
|
||||
@@ -694,10 +696,12 @@ public sealed class DriverNodeManager : CustomNodeManager2, IAddressSpaceBuilder
|
||||
|
||||
var request = methodsToCall[i];
|
||||
|
||||
// Only handle the two well-known Part 9 method ids.
|
||||
// Only handle the well-known Part 9 method ids. AddComment is declared on the
|
||||
// base ConditionType; Acknowledge / Confirm on AcknowledgeableConditionType.
|
||||
var isAcknowledge = request.MethodId == MethodIds.AcknowledgeableConditionType_Acknowledge;
|
||||
var isConfirm = request.MethodId == MethodIds.AcknowledgeableConditionType_Confirm;
|
||||
if (!isAcknowledge && !isConfirm) continue;
|
||||
var isAddComment = request.MethodId == MethodIds.ConditionType_AddComment;
|
||||
if (!isAcknowledge && !isConfirm && !isAddComment) continue;
|
||||
|
||||
// ObjectId must be a string identifier so we can look it up in the index.
|
||||
if (request.ObjectId.Identifier is not string conditionKey) continue;
|
||||
@@ -717,9 +721,14 @@ public sealed class DriverNodeManager : CustomNodeManager2, IAddressSpaceBuilder
|
||||
if (isAcknowledge)
|
||||
engine.AcknowledgeAsync(alarmId, user, comment, CancellationToken.None)
|
||||
.GetAwaiter().GetResult();
|
||||
else
|
||||
else if (isConfirm)
|
||||
engine.ConfirmAsync(alarmId, user, comment, CancellationToken.None)
|
||||
.GetAwaiter().GetResult();
|
||||
else
|
||||
// AddComment requires comment text — the engine's Part 9 state machine
|
||||
// rejects null/empty with ArgumentException, surfaced as BadInvalidArgument.
|
||||
engine.AddCommentAsync(alarmId, user, comment ?? string.Empty, CancellationToken.None)
|
||||
.GetAwaiter().GetResult();
|
||||
|
||||
// Mark the slot as handled so base.Call skips it. A pre-populated Good
|
||||
// result (not null and not Bad) is the signal the base class uses to
|
||||
@@ -904,6 +913,10 @@ public sealed class DriverNodeManager : CustomNodeManager2, IAddressSpaceBuilder
|
||||
return OpcUaOperation.AlarmAcknowledge;
|
||||
if (methodId == MethodIds.AcknowledgeableConditionType_Confirm)
|
||||
return OpcUaOperation.AlarmConfirm;
|
||||
// AddComment (ConditionType) has no dedicated operation/permission bit — it gates at
|
||||
// the same operator tier as Acknowledge, the closest existing alarm-action grant.
|
||||
if (methodId == MethodIds.ConditionType_AddComment)
|
||||
return OpcUaOperation.AlarmAcknowledge;
|
||||
// Shelve methods live on each alarm's own ShelvedStateMachine subtree, so they're
|
||||
// matched by NodeId membership rather than a constant comparison.
|
||||
if (methodId is not null && shelveMethodIds is not null && shelveMethodIds.Contains(methodId))
|
||||
|
||||
Reference in New Issue
Block a user