feat(scripted-alarms): richer AlarmConditionState bridge to the OPC UA node (T15)
This commit is contained in:
@@ -90,42 +90,69 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply an alarm-state write. When a real Part 9 <see cref="AlarmConditionState"/> has been
|
||||
/// materialised for <paramref name="alarmNodeId"/> (via <see cref="MaterialiseAlarmCondition"/>),
|
||||
/// this projects <paramref name="active"/>/<paramref name="acknowledged"/> onto the live
|
||||
/// condition node's ActiveState/AckedState/Retain (T14 — basic active/ack only; <b>no event
|
||||
/// firing yet</b>, that lands in T16). Otherwise it falls back to the legacy two-element
|
||||
/// <c>[active, acknowledged]</c> <see cref="BaseDataVariableState"/> placeholder so callers
|
||||
/// whose alarm node hasn't been materialised (and the existing unit tests) keep working.
|
||||
/// Apply a full Part 9 alarm-condition write. When a real <see cref="AlarmConditionState"/> has
|
||||
/// been materialised for <paramref name="alarmNodeId"/> (via <see cref="MaterialiseAlarmCondition"/>),
|
||||
/// this projects the whole <paramref name="state"/> snapshot
|
||||
/// (Enabled / Active / Acked / Confirmed / Shelving / Severity / Message) onto the live condition
|
||||
/// node and recomputes Retain (T15 — richer state; <b>still no event firing</b>, that lands in T16).
|
||||
/// Otherwise it falls back to the legacy two-element <c>[Active, Acknowledged]</c>
|
||||
/// <see cref="BaseDataVariableState"/> placeholder so callers whose alarm node hasn't been
|
||||
/// materialised (and the existing unit tests) keep working.
|
||||
/// </summary>
|
||||
/// <param name="alarmNodeId">The node identifier of the alarm (== ScriptedAlarmId for materialised conditions).</param>
|
||||
/// <param name="active">Whether the alarm is currently active.</param>
|
||||
/// <param name="acknowledged">Whether the alarm has been acknowledged.</param>
|
||||
/// <param name="state">The full condition state to project onto the node.</param>
|
||||
/// <param name="sourceTimestampUtc">The timestamp of the alarm state change in UTC.</param>
|
||||
public void WriteAlarmState(string alarmNodeId, bool active, bool acknowledged, DateTime sourceTimestampUtc)
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(alarmNodeId);
|
||||
ArgumentNullException.ThrowIfNull(state);
|
||||
|
||||
if (_alarmConditions.TryGetValue(alarmNodeId, out var condition))
|
||||
{
|
||||
lock (Lock)
|
||||
{
|
||||
condition.SetActiveState(SystemContext, active);
|
||||
condition.SetAcknowledgedState(SystemContext, acknowledged);
|
||||
// EnabledState / AckedState / ActiveState are mandatory children — always present after
|
||||
// Create. Confirm + Shelving are optional Part 9 children: T14's real-server finding is
|
||||
// that Create auto-builds them for our subtypes, but a base AlarmConditionState (or a
|
||||
// future SDK that builds a leaner child set) may leave them null. Null-guard each optional
|
||||
// child so projecting Confirmed/Shelving onto a node that lacks the sub-state machine is a
|
||||
// no-op rather than an NRE.
|
||||
condition.SetEnableState(SystemContext, state.Enabled);
|
||||
condition.SetActiveState(SystemContext, state.Active);
|
||||
condition.SetAcknowledgedState(SystemContext, state.Acknowledged);
|
||||
if (condition.ConfirmedState is not null)
|
||||
{
|
||||
condition.SetConfirmedState(SystemContext, state.Confirmed);
|
||||
}
|
||||
if (condition.ShelvingState is not null)
|
||||
{
|
||||
// SetShelvingState(shelved, oneShot, shelvingTime): map our 3-way kind onto the SDK's
|
||||
// (shelved, oneShot) flag pair. Timed shelving's expiry is owned by the engine, not the
|
||||
// SDK timer, so we pass shelvingTime=0 (no SDK-managed auto-unshelve).
|
||||
condition.SetShelvingState(
|
||||
SystemContext,
|
||||
shelved: state.Shelving != AlarmShelvingKind.Unshelved,
|
||||
oneShot: state.Shelving == AlarmShelvingKind.OneShot,
|
||||
shelvingTime: 0);
|
||||
}
|
||||
condition.SetSeverity(SystemContext, MapSeverity(state.Severity));
|
||||
condition.Message.Value = new LocalizedText(state.Message);
|
||||
|
||||
// Part 9: retain the condition while it is active OR unacknowledged so a client's
|
||||
// ConditionRefresh replays it. T16's event firing will also drive Retain; here we keep
|
||||
// it correct for the basic projection.
|
||||
condition.Retain.Value = active || !acknowledged;
|
||||
// it correct for the projection.
|
||||
condition.Retain.Value = state.Active || !state.Acknowledged;
|
||||
condition.Time.Value = sourceTimestampUtc;
|
||||
condition.ReceiveTime.Value = sourceTimestampUtc;
|
||||
// NO ReportEvent here — T16 owns event firing. ClearChangeMasks just notifies any
|
||||
// attribute (not event) subscribers watching ActiveState/AckedState/Retain directly.
|
||||
// attribute (not event) subscribers watching the condition's children directly.
|
||||
condition.ClearChangeMasks(SystemContext, includeChildren: true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: alarm not materialised as a real condition — keep the legacy bool[2] variable.
|
||||
// Fallback: alarm not materialised as a real condition — keep the legacy bool[2] variable so
|
||||
// un-materialised callers (and the existing unit tests) keep working.
|
||||
lock (Lock)
|
||||
{
|
||||
// CreateVariable mutates the SDK address space, so it MUST run under Lock (see WriteValue).
|
||||
@@ -135,7 +162,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
_variables[alarmNodeId] = variable;
|
||||
}
|
||||
|
||||
variable.Value = new[] { active, acknowledged };
|
||||
variable.Value = new[] { state.Active, state.Acknowledged };
|
||||
variable.StatusCode = StatusCodes.Good;
|
||||
variable.Timestamp = sourceTimestampUtc;
|
||||
variable.ClearChangeMasks(SystemContext, includeChildren: false);
|
||||
@@ -146,7 +173,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// Materialise a real OPC UA Part 9 <see cref="AlarmConditionState"/> node under its equipment
|
||||
/// folder so clients can browse it as a proper condition (and, once T16 lands, subscribe to its
|
||||
/// events). The node id is the alarm node id (the ScriptedAlarmId) so subsequent
|
||||
/// <see cref="WriteAlarmState"/> calls — which target that same id — update this node.
|
||||
/// <see cref="WriteAlarmCondition"/> calls — which target that same id — update this node.
|
||||
/// <para>
|
||||
/// This is the T14 production replacement for the <c>bool[2]</c> placeholder: it creates
|
||||
/// node + basic Active/Ack state + the notifier wiring needed for T16 events, but fires
|
||||
|
||||
Reference in New Issue
Block a user