merge WP2 (node-manager dual-namespace + sink realm surface) into v3/batch4-address-space
This commit is contained in:
@@ -23,30 +23,30 @@ public sealed class DeferredAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgical
|
||||
_inner = sink ?? NullOpcUaAddressSpaceSink.Instance;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc)
|
||||
=> _inner.WriteValue(nodeId, value, quality, sourceTimestampUtc);
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _inner.WriteValue(nodeId, value, quality, sourceTimestampUtc, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc)
|
||||
=> _inner.WriteAlarmCondition(alarmNodeId, state, sourceTimestampUtc);
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _inner.WriteAlarmCondition(alarmNodeId, state, sourceTimestampUtc, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false)
|
||||
=> _inner.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative);
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _inner.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName)
|
||||
=> _inner.EnsureFolder(folderNodeId, parentNodeId, displayName);
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _inner.EnsureFolder(folderNodeId, parentNodeId, displayName, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null)
|
||||
=> _inner.EnsureVariable(variableNodeId, parentFolderNodeId, displayName, dataType, writable, historianTagname, isArray, arrayLength);
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _inner.EnsureVariable(variableNodeId, parentFolderNodeId, displayName, dataType, writable, historianTagname, isArray, arrayLength, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RebuildAddressSpace() => _inner.RebuildAddressSpace();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId) => _inner.RaiseNodesAddedModelChange(affectedNodeId);
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => _inner.RaiseNodesAddedModelChange(affectedNodeId, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
// Forwards to the inner sink when it supports the surgical capability; returns false otherwise —
|
||||
@@ -55,9 +55,9 @@ public sealed class DeferredAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgical
|
||||
// Without this forward the surgical optimization is inert on every driver-role host, because
|
||||
// actors inject THIS wrapper, not the inner sink. ALL six args (including the DataType/array-shape
|
||||
// ones) MUST be forwarded — a partial forward silently drops the shape update on every driver-role host.
|
||||
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength)
|
||||
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _inner is ISurgicalAddressSpaceSink surgical
|
||||
&& surgical.UpdateTagAttributes(variableNodeId, writable, historianTagname, dataType, isArray, arrayLength);
|
||||
&& surgical.UpdateTagAttributes(variableNodeId, writable, historianTagname, dataType, isArray, arrayLength, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
// Forwards to the inner sink when it supports the surgical capability; returns false otherwise —
|
||||
@@ -65,9 +65,9 @@ public sealed class DeferredAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgical
|
||||
// sink that isn't surgical — so the caller (AddressSpaceApplier) falls back to a full rebuild.
|
||||
// Without this forward the rename-refresh optimization is inert on every driver-role host, because
|
||||
// actors inject THIS wrapper, not the inner sink.
|
||||
public bool UpdateFolderDisplayName(string folderNodeId, string displayName)
|
||||
public bool UpdateFolderDisplayName(string folderNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _inner is ISurgicalAddressSpaceSink surgical
|
||||
&& surgical.UpdateFolderDisplayName(folderNodeId, displayName);
|
||||
&& surgical.UpdateFolderDisplayName(folderNodeId, displayName, realm);
|
||||
|
||||
// R2-07 T7 — surgical remove forwards. Same capability-sniffing pattern as UpdateTagAttributes /
|
||||
// UpdateFolderDisplayName: forward when the inner sink supports the surgical capability; return false
|
||||
@@ -75,14 +75,14 @@ public sealed class DeferredAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgical
|
||||
// (AddressSpaceApplier) falls back to a full rebuild. Without these forwards the surgical remove path is
|
||||
// inert on every driver-role host — the F10b / PR#423 trap the reflection guard exists to catch.
|
||||
/// <inheritdoc />
|
||||
public bool RemoveVariableNode(string variableNodeId)
|
||||
=> _inner is ISurgicalAddressSpaceSink surgical && surgical.RemoveVariableNode(variableNodeId);
|
||||
public bool RemoveVariableNode(string variableNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _inner is ISurgicalAddressSpaceSink surgical && surgical.RemoveVariableNode(variableNodeId, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool RemoveAlarmConditionNode(string alarmNodeId)
|
||||
=> _inner is ISurgicalAddressSpaceSink surgical && surgical.RemoveAlarmConditionNode(alarmNodeId);
|
||||
public bool RemoveAlarmConditionNode(string alarmNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _inner is ISurgicalAddressSpaceSink surgical && surgical.RemoveAlarmConditionNode(alarmNodeId, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool RemoveEquipmentSubtree(string equipmentNodeId)
|
||||
=> _inner is ISurgicalAddressSpaceSink surgical && surgical.RemoveEquipmentSubtree(equipmentNodeId);
|
||||
public bool RemoveEquipmentSubtree(string equipmentNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _inner is ISurgicalAddressSpaceSink surgical && surgical.RemoveEquipmentSubtree(equipmentNodeId, realm);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,12 @@ public interface IOpcUaAddressSpaceSink
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="quality">The quality status of the value.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc);
|
||||
/// <param name="realm">Which of the two v3 namespaces (<see cref="AddressSpaceRealm.Raw"/> device tree or
|
||||
/// <see cref="AddressSpaceRealm.Uns"/> equipment tree) the <paramref name="nodeId"/> lives in — the sink
|
||||
/// resolves the namespace index from the realm rather than parsing it out of the id string. WP3's fan-out
|
||||
/// calls this once per NodeId (raw + every referencing UNS node) with the matching realm.</param>
|
||||
// transitional default — B4-WP3/Wave B makes every call site explicit and removes this default.
|
||||
void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns);
|
||||
|
||||
/// <summary>Write an alarm-condition's full Part 9 state. When a real condition node has been
|
||||
/// materialised for <paramref name="alarmNodeId"/> via <see cref="MaterialiseAlarmCondition"/>,
|
||||
@@ -25,7 +30,10 @@ public interface IOpcUaAddressSpaceSink
|
||||
/// <param name="alarmNodeId">The OPC UA node ID of the alarm (== ScriptedAlarmId for materialised conditions).</param>
|
||||
/// <param name="state">The full condition state to project onto the node.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc);
|
||||
/// <param name="realm">The namespace realm <paramref name="alarmNodeId"/> lives in (must match the realm
|
||||
/// the condition was materialised under).</param>
|
||||
// transitional default — B4-WP3/Wave B makes every call site explicit and removes this default.
|
||||
void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns);
|
||||
|
||||
/// <summary>
|
||||
/// Materialise a real OPC UA Part 9 alarm-condition node under its equipment folder so clients
|
||||
@@ -41,7 +49,10 @@ public interface IOpcUaAddressSpaceSink
|
||||
/// <param name="isNative">When true this is a driver-fed (native, e.g. Galaxy) equipment-tag alarm; when
|
||||
/// false (default) it is a scripted alarm. The node manager tracks native condition node ids so a later
|
||||
/// task can route a native condition's inbound Acknowledge to the driver instead of the scripted engine.</param>
|
||||
void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false);
|
||||
/// <param name="realm">The namespace realm the condition (and its parent folder <paramref name="equipmentNodeId"/>)
|
||||
/// live in.</param>
|
||||
// transitional default — B4-WP3/Wave B makes every call site explicit and removes this default.
|
||||
void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns);
|
||||
|
||||
/// <summary>
|
||||
/// Ensure a folder node exists under the given parent. Used by <c>AddressSpaceApplier</c> to
|
||||
@@ -52,7 +63,9 @@ public interface IOpcUaAddressSpaceSink
|
||||
/// <param name="folderNodeId">The OPC UA node ID for the folder.</param>
|
||||
/// <param name="parentNodeId">The parent folder node ID, or null for namespace root.</param>
|
||||
/// <param name="displayName">The display name for the folder.</param>
|
||||
void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName);
|
||||
/// <param name="realm">The namespace realm the folder (and its <paramref name="parentNodeId"/>) live in.</param>
|
||||
// transitional default — B4-WP3/Wave B makes every call site explicit and removes this default.
|
||||
void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns);
|
||||
|
||||
/// <summary>
|
||||
/// Ensure a Variable node exists at <paramref name="variableNodeId"/>, parented under
|
||||
@@ -77,7 +90,11 @@ public interface IOpcUaAddressSpaceSink
|
||||
/// rank + dimensions carry the array-ness.</param>
|
||||
/// <param name="arrayLength">The declared length of the 1-D array when <paramref name="isArray"/> is
|
||||
/// true; ignored for scalars. Null ⇒ length 0 (unbounded).</param>
|
||||
void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null);
|
||||
/// <param name="realm">The namespace realm the variable (and its <paramref name="parentFolderNodeId"/>) live
|
||||
/// in. A historized UNS reference node registers the SAME <paramref name="historianTagname"/> as its backing
|
||||
/// raw node — both NodeIds map to one tagname — so this call carries the shared tagname per realm.</param>
|
||||
// transitional default — B4-WP3/Wave B makes every call site explicit and removes this default.
|
||||
void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns);
|
||||
|
||||
/// <summary>
|
||||
/// Tear down + repopulate the address space. Called by <c>OpcUaPublishActor</c> after a
|
||||
@@ -91,7 +108,9 @@ public interface IOpcUaAddressSpaceSink
|
||||
/// (Part 3 GeneralModelChangeEvent, verb NodeAdded).
|
||||
/// </summary>
|
||||
/// <param name="affectedNodeId">The node under which discovered nodes were added.</param>
|
||||
void RaiseNodesAddedModelChange(string affectedNodeId);
|
||||
/// <param name="realm">The namespace realm <paramref name="affectedNodeId"/> lives in.</param>
|
||||
// transitional default — B4-WP3/Wave B makes every call site explicit and removes this default.
|
||||
void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns);
|
||||
}
|
||||
|
||||
/// <summary>OPC UA status code projection — Good / Uncertain / Bad. Real SDK has finer-grained
|
||||
@@ -106,23 +125,23 @@ public sealed class NullOpcUaAddressSpaceSink : IOpcUaAddressSpaceSink
|
||||
private NullOpcUaAddressSpaceSink() { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc) { }
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) { }
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null) { }
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RebuildAddressSpace() { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId) { }
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
}
|
||||
|
||||
@@ -21,8 +21,11 @@ public interface ISurgicalAddressSpaceSink
|
||||
/// <param name="dataType">The OPC UA built-in data type name (e.g. "Boolean", "Int32", "Float").</param>
|
||||
/// <param name="isArray">When true the node is a 1-D array (ValueRank=OneDimension); when false scalar.</param>
|
||||
/// <param name="arrayLength">The declared length of the 1-D array when <paramref name="isArray"/> is true; ignored for scalars.</param>
|
||||
/// <param name="realm">The namespace realm <paramref name="variableNodeId"/> lives in — the sink resolves the
|
||||
/// namespace index from the realm rather than parsing it out of the id string.</param>
|
||||
/// <returns>True when the in-place update was applied; false when the node is missing (caller rebuilds).</returns>
|
||||
bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength);
|
||||
// transitional default — B4-WP3/Wave B makes every call site explicit and removes this default.
|
||||
bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength, AddressSpaceRealm realm = AddressSpaceRealm.Uns);
|
||||
|
||||
/// <summary>Update an existing folder node's <c>DisplayName</c> IN PLACE, notifying subscribers
|
||||
/// (ClearChangeMasks) without a rebuild — used by AddressSpaceApplier to apply a UNS Area / Line
|
||||
@@ -32,8 +35,10 @@ public interface ISurgicalAddressSpaceSink
|
||||
/// should rebuild instead).</summary>
|
||||
/// <param name="folderNodeId">The node id of the folder whose display name to update in place.</param>
|
||||
/// <param name="displayName">The new display name to apply.</param>
|
||||
/// <param name="realm">The namespace realm <paramref name="folderNodeId"/> lives in.</param>
|
||||
/// <returns>True when the in-place update was applied; false when the folder is missing (caller rebuilds).</returns>
|
||||
bool UpdateFolderDisplayName(string folderNodeId, string displayName);
|
||||
// transitional default — B4-WP3/Wave B makes every call site explicit and removes this default.
|
||||
bool UpdateFolderDisplayName(string folderNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns);
|
||||
|
||||
/// <summary>Remove ONE existing value-variable node IN PLACE (detach from its parent, drop it from the
|
||||
/// predefined-node set, drop any historized-tagname registration), then raise a Part 3
|
||||
@@ -43,8 +48,10 @@ public interface ISurgicalAddressSpaceSink
|
||||
/// re-subscription attempts get BadNodeIdUnknown from the SDK. Returns false when the node id is unknown
|
||||
/// (the node-manager maps drifted from the plan — caller falls back to a full rebuild).</summary>
|
||||
/// <param name="variableNodeId">The folder-scoped node id of the variable to remove in place.</param>
|
||||
/// <param name="realm">The namespace realm <paramref name="variableNodeId"/> lives in.</param>
|
||||
/// <returns>True when the node was removed; false when the id is unknown (caller rebuilds).</returns>
|
||||
bool RemoveVariableNode(string variableNodeId);
|
||||
// transitional default — B4-WP3/Wave B makes every call site explicit and removes this default.
|
||||
bool RemoveVariableNode(string variableNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns);
|
||||
|
||||
/// <summary>Remove ONE existing Part 9 alarm-condition node IN PLACE (detach, drop from the
|
||||
/// predefined-node set, clear the native-alarm flag), then raise NodeDeleted outside the lock. The
|
||||
@@ -52,8 +59,10 @@ public interface ISurgicalAddressSpaceSink
|
||||
/// stops replaying on ConditionRefresh. Returns false when the condition id is unknown (caller
|
||||
/// rebuilds).</summary>
|
||||
/// <param name="alarmNodeId">The alarm condition node id (== ScriptedAlarmId, or a native alarm tag's folder-scoped id).</param>
|
||||
/// <param name="realm">The namespace realm <paramref name="alarmNodeId"/> lives in.</param>
|
||||
/// <returns>True when the condition was removed; false when the id is unknown (caller rebuilds).</returns>
|
||||
bool RemoveAlarmConditionNode(string alarmNodeId);
|
||||
// transitional default — B4-WP3/Wave B makes every call site explicit and removes this default.
|
||||
bool RemoveAlarmConditionNode(string alarmNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns);
|
||||
|
||||
/// <summary>Remove an equipment folder AND its entire descendant subtree (sub-folders, value variables,
|
||||
/// condition nodes) IN PLACE: per-descendant map/registration cleanup (variables, historized tagnames,
|
||||
@@ -62,6 +71,8 @@ public interface ISurgicalAddressSpaceSink
|
||||
/// the equipment folder outside the lock. Returns false when the folder id is unknown (caller
|
||||
/// rebuilds).</summary>
|
||||
/// <param name="equipmentNodeId">The equipment folder node id whose entire subtree to remove.</param>
|
||||
/// <param name="realm">The namespace realm <paramref name="equipmentNodeId"/> lives in.</param>
|
||||
/// <returns>True when the subtree was removed; false when the id is unknown (caller rebuilds).</returns>
|
||||
bool RemoveEquipmentSubtree(string equipmentNodeId);
|
||||
// transitional default — B4-WP3/Wave B makes every call site explicit and removes this default.
|
||||
bool RemoveEquipmentSubtree(string equipmentNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,20 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
/// </summary>
|
||||
public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
{
|
||||
public const string DefaultNamespaceUri = "https://zb.com/otopcua/ns";
|
||||
/// <summary>v3 dual-namespace: both the Raw (device tree) and UNS (equipment tree) namespaces are
|
||||
/// registered with the SDK so nodes can be minted in either. <see cref="V3NodeIds.RawNamespaceUri"/> is
|
||||
/// registered FIRST (namespace index <see cref="NamespaceIndexes"/>[0]) and
|
||||
/// <see cref="V3NodeIds.UnsNamespaceUri"/> SECOND ([1]); <see cref="NamespaceIndexForRealm"/> maps a realm
|
||||
/// to its index. Which realm a node belongs to travels with each sink call as an
|
||||
/// <see cref="AddressSpaceRealm"/> — the namespace is never parsed back out of the id string.</summary>
|
||||
private static readonly string[] RegisteredNamespaceUris = { V3NodeIds.RawNamespaceUri, V3NodeIds.UnsNamespaceUri };
|
||||
|
||||
/// <summary>Transitional alias kept so the pre-v3 single-namespace integration tests
|
||||
/// (<c>SubscriptionSurvivalTests</c>) still resolve a namespace index. It points at
|
||||
/// <see cref="V3NodeIds.UnsNamespaceUri"/> — the same realm as the transitional default of every sink
|
||||
/// method — so those tests keep resolving the namespace the applier's default-realm nodes live in. The two
|
||||
/// v3 URIs are the real namespaces; this alias is removed once those tests target Raw/UNS explicitly.</summary>
|
||||
public const string DefaultNamespaceUri = V3NodeIds.UnsNamespaceUri;
|
||||
|
||||
private readonly ConcurrentDictionary<string, BaseDataVariableState> _variables = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, FolderState> _folders = new(StringComparer.Ordinal);
|
||||
@@ -73,11 +86,47 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// <param name="server">The OPC UA server instance.</param>
|
||||
/// <param name="configuration">The application configuration.</param>
|
||||
public OtOpcUaNodeManager(IServerInternal server, ApplicationConfiguration configuration)
|
||||
: base(server, configuration, DefaultNamespaceUri)
|
||||
: base(server, configuration, RegisteredNamespaceUris)
|
||||
{
|
||||
// SystemContext is initialised by the base ctor.
|
||||
// SystemContext is initialised by the base ctor; both namespace URIs are now registered and their
|
||||
// indexes captured (in the order passed) in NamespaceIndexes.
|
||||
}
|
||||
|
||||
/// <summary>Resolve a realm to the SDK namespace index its nodes are minted under. Raw is registered
|
||||
/// first (<see cref="NamespaceIndexes"/>[0]), UNS second ([1]) — see <see cref="RegisteredNamespaceUris"/>.</summary>
|
||||
/// <param name="realm">The address-space realm.</param>
|
||||
/// <returns>The namespace index for the realm.</returns>
|
||||
internal ushort NamespaceIndexForRealm(AddressSpaceRealm realm) => realm switch
|
||||
{
|
||||
AddressSpaceRealm.Raw => NamespaceIndexes[0],
|
||||
AddressSpaceRealm.Uns => NamespaceIndexes[1],
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(realm), realm, "Unknown address-space realm."),
|
||||
};
|
||||
|
||||
/// <summary>Recover the realm of an already-resolved SDK <see cref="NodeId"/> from its namespace index —
|
||||
/// used at the SDK-facing seams (inbound write, HistoryRead) where the request carries a full NodeId.
|
||||
/// Any index that is not the Raw namespace is treated as UNS (the two registered indexes are the only
|
||||
/// ones this manager mints into).</summary>
|
||||
/// <param name="nodeId">The resolved SDK node id.</param>
|
||||
/// <returns>The realm the node belongs to.</returns>
|
||||
private AddressSpaceRealm RealmOf(NodeId nodeId) =>
|
||||
nodeId.NamespaceIndex == NamespaceIndexes[0] ? AddressSpaceRealm.Raw : AddressSpaceRealm.Uns;
|
||||
|
||||
/// <summary>Build the internal map key for a realm-qualified string id. The key is the node's full
|
||||
/// namespace-qualified NodeId string (e.g. <c>ns=2;s=<id></c>) so a Raw node and a UNS node that
|
||||
/// share the same <c>s=</c> identifier are distinct keys — a bare id is no longer globally unique across
|
||||
/// the two namespaces. Matches <see cref="MapKey(NodeId)"/> for the same node.</summary>
|
||||
/// <param name="realm">The realm the id lives in.</param>
|
||||
/// <param name="id">The bare <c>s=</c> identifier.</param>
|
||||
/// <returns>The namespace-qualified map key.</returns>
|
||||
private string MapKey(AddressSpaceRealm realm, string id) => new NodeId(id, NamespaceIndexForRealm(realm)).ToString();
|
||||
|
||||
/// <summary>Build the internal map key for an already-resolved SDK <see cref="NodeId"/> (the SDK-facing
|
||||
/// seams). Equal to <see cref="MapKey(AddressSpaceRealm, string)"/> for the same node.</summary>
|
||||
/// <param name="nodeId">The resolved SDK node id.</param>
|
||||
/// <returns>The namespace-qualified map key.</returns>
|
||||
private static string MapKey(NodeId nodeId) => nodeId.ToString();
|
||||
|
||||
/// <summary>Gets the count of variable nodes currently managed.</summary>
|
||||
public int VariableCount => _variables.Count;
|
||||
/// <summary>Gets the count of folder nodes currently managed.</summary>
|
||||
@@ -246,8 +295,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// ScriptedAlarmId), or null if not yet materialised. Exposed for tests + diagnostics.</summary>
|
||||
/// <param name="alarmNodeId">The alarm node identifier (== ScriptedAlarmId).</param>
|
||||
/// <returns>The cached <see cref="AlarmConditionState"/>, or null when none is registered.</returns>
|
||||
public AlarmConditionState? TryGetAlarmCondition(string alarmNodeId) =>
|
||||
_alarmConditions.TryGetValue(alarmNodeId, out var condition) ? condition : null;
|
||||
public AlarmConditionState? TryGetAlarmCondition(string alarmNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) =>
|
||||
_alarmConditions.TryGetValue(MapKey(realm, alarmNodeId), out var condition) ? condition : null;
|
||||
|
||||
/// <summary>Phase C: look up the resolved historian tagname registered for a historized variable
|
||||
/// node, or null when the node is not historized. The (later) HistoryRead override resolves an
|
||||
@@ -255,9 +304,23 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// <param name="nodeId">The variable node identifier.</param>
|
||||
/// <param name="tagname">The resolved historian tagname when historized; otherwise null.</param>
|
||||
/// <returns>True when the node is registered as historized; otherwise false.</returns>
|
||||
public bool TryGetHistorizedTagname(string nodeId, out string? tagname)
|
||||
public bool TryGetHistorizedTagname(string nodeId, out string? tagname, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
if (_historizedTagnames.TryGetValue(nodeId, out var t)) { tagname = t; return true; }
|
||||
if (_historizedTagnames.TryGetValue(MapKey(realm, nodeId), out var t)) { tagname = t; return true; }
|
||||
tagname = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>NodeId-typed overload used by the HistoryRead override, which holds a request's full
|
||||
/// (namespace-qualified) NodeId — resolves the historian tagname without recovering the realm first.
|
||||
/// A historized UNS reference node and its backing raw node register the SAME tagname under distinct
|
||||
/// NodeIds, so a read against either resolves the one series.</summary>
|
||||
/// <param name="nodeId">The resolved SDK node id from the HistoryRead request.</param>
|
||||
/// <param name="tagname">The resolved historian tagname when historized; otherwise null.</param>
|
||||
/// <returns>True when the node is registered as historized; otherwise false.</returns>
|
||||
private bool TryGetHistorizedTagname(NodeId nodeId, out string? tagname)
|
||||
{
|
||||
if (_historizedTagnames.TryGetValue(MapKey(nodeId), out var t)) { tagname = t; return true; }
|
||||
tagname = null;
|
||||
return false;
|
||||
}
|
||||
@@ -266,16 +329,16 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// Exposed for tests so they can assert the SDK node's Historizing / AccessLevel attributes.</summary>
|
||||
/// <param name="nodeId">The variable node identifier.</param>
|
||||
/// <returns>The cached <see cref="BaseDataVariableState"/>, or null when none is registered.</returns>
|
||||
internal BaseDataVariableState? TryGetVariable(string nodeId) =>
|
||||
_variables.TryGetValue(nodeId, out var variable) ? variable : null;
|
||||
internal BaseDataVariableState? TryGetVariable(string nodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) =>
|
||||
_variables.TryGetValue(MapKey(realm, nodeId), out var variable) ? variable : null;
|
||||
|
||||
/// <summary>Look up a materialised folder node by its NodeId string, or null if not present.
|
||||
/// Exposed for tests so they can resolve an equipment folder's NodeId (e.g. the event-notifier
|
||||
/// node a HistoryReadEvents request targets).</summary>
|
||||
/// <param name="nodeId">The folder node identifier.</param>
|
||||
/// <returns>The cached <see cref="FolderState"/>, or null when none is registered.</returns>
|
||||
internal FolderState? TryGetFolder(string nodeId) =>
|
||||
_folders.TryGetValue(nodeId, out var folder) ? folder : null;
|
||||
internal FolderState? TryGetFolder(string nodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) =>
|
||||
_folders.TryGetValue(MapKey(realm, nodeId), out var folder) ? folder : null;
|
||||
|
||||
/// <summary>
|
||||
/// Apply a value write from <see cref="IOpcUaAddressSpaceSink.WriteValue"/>. Creates the
|
||||
@@ -286,19 +349,21 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// <param name="value">The new value to write.</param>
|
||||
/// <param name="quality">The OPC UA quality status code.</param>
|
||||
/// <param name="sourceTimestampUtc">The timestamp of the value in UTC.</param>
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc)
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(nodeId);
|
||||
EnsureAddressSpaceCreated();
|
||||
|
||||
var ns = NamespaceIndexForRealm(realm);
|
||||
var key = MapKey(realm, nodeId);
|
||||
lock (Lock)
|
||||
{
|
||||
// CreateVariable mutates the SDK address space (_root.AddChild + AddPredefinedNode),
|
||||
// so it MUST run under Lock — the SDK's subscription/ConditionRefresh threads take it too.
|
||||
if (!_variables.TryGetValue(nodeId, out var variable))
|
||||
if (!_variables.TryGetValue(key, out var variable))
|
||||
{
|
||||
variable = CreateVariable(nodeId);
|
||||
_variables[nodeId] = variable;
|
||||
variable = CreateVariable(nodeId, ns);
|
||||
_variables[key] = variable;
|
||||
}
|
||||
|
||||
variable.Value = value;
|
||||
@@ -321,17 +386,19 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// <param name="alarmNodeId">The node identifier of the alarm (== ScriptedAlarmId for materialised conditions).</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 WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc)
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(alarmNodeId);
|
||||
ArgumentNullException.ThrowIfNull(state);
|
||||
EnsureAddressSpaceCreated();
|
||||
|
||||
var ns = NamespaceIndexForRealm(realm);
|
||||
var key = MapKey(realm, alarmNodeId);
|
||||
// Look up + project under a SINGLE Lock so a concurrent RebuildAddressSpace can't clear
|
||||
// _alarmConditions / detach the condition node between the lookup and the Set* calls.
|
||||
lock (Lock)
|
||||
{
|
||||
if (_alarmConditions.TryGetValue(alarmNodeId, out var condition))
|
||||
if (_alarmConditions.TryGetValue(key, out var condition))
|
||||
{
|
||||
// T20 delta-gate: read the node's CURRENT live condition state FIRST (before projecting
|
||||
// the incoming snapshot onto it), then decide fire-vs-suppress by comparing the incoming
|
||||
@@ -396,10 +463,10 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// 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. CreateVariable mutates
|
||||
// the SDK address space, so it MUST run under Lock (see WriteValue).
|
||||
if (!_variables.TryGetValue(alarmNodeId, out var variable))
|
||||
if (!_variables.TryGetValue(key, out var variable))
|
||||
{
|
||||
variable = CreateVariable(alarmNodeId);
|
||||
_variables[alarmNodeId] = variable;
|
||||
variable = CreateVariable(alarmNodeId, ns);
|
||||
_variables[key] = variable;
|
||||
}
|
||||
|
||||
variable.Value = new[] { state.Active, state.Acknowledged };
|
||||
@@ -611,12 +678,14 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// <see cref="AlarmConditionState"/>. LimitAlarm deliberately falls back to base per the T13
|
||||
/// notes — a script alarm carries no High/Low limits to populate.</para>
|
||||
/// </remarks>
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false)
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(alarmNodeId);
|
||||
ArgumentException.ThrowIfNullOrEmpty(displayName);
|
||||
EnsureAddressSpaceCreated();
|
||||
|
||||
var ns = NamespaceIndexForRealm(realm);
|
||||
var conditionKey = MapKey(realm, alarmNodeId);
|
||||
lock (Lock)
|
||||
{
|
||||
// R2-07 T4a: idempotent skip-if-present-and-same-kind. On a PURE-ADD apply,
|
||||
@@ -627,12 +696,12 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// re-severity/type change arrives as a ChangedAlarms delta ⇒ full rebuild (the map is cleared
|
||||
// first), so it never reaches this path with a stale-but-present entry; the drop-and-recreate
|
||||
// stays ONLY for the kind-swap (native↔scripted) case, which flips the native flag.
|
||||
if (_alarmConditions.ContainsKey(alarmNodeId) && _nativeAlarmNodeIds.Contains(alarmNodeId) == isNative)
|
||||
if (_alarmConditions.ContainsKey(conditionKey) && _nativeAlarmNodeIds.Contains(conditionKey) == isNative)
|
||||
return;
|
||||
|
||||
// Idempotent: drop any prior node for this id so a re-materialise (e.g. changed
|
||||
// type/severity on redeploy) reflects cleanly instead of leaking the old node.
|
||||
if (_alarmConditions.TryRemove(alarmNodeId, out var existing))
|
||||
if (_alarmConditions.TryRemove(conditionKey, out var existing))
|
||||
{
|
||||
existing.Parent?.RemoveChild(existing);
|
||||
PredefinedNodes?.Remove(existing.NodeId);
|
||||
@@ -640,9 +709,9 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
|
||||
// H6a: re-materialising the same id as the OTHER kind (native↔scripted) must reflect the new
|
||||
// kind, so always drop the stale native flag first and only re-add it below when isNative.
|
||||
_nativeAlarmNodeIds.Remove(alarmNodeId);
|
||||
_nativeAlarmNodeIds.Remove(conditionKey);
|
||||
|
||||
var parent = ResolveParentFolder(equipmentNodeId);
|
||||
var parent = ResolveParentFolder(equipmentNodeId, realm);
|
||||
|
||||
AlarmConditionState alarm = CreateAlarmConditionOfType(alarmType, parent);
|
||||
alarm.SymbolicName = displayName;
|
||||
@@ -654,8 +723,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// type's embedded definition; we do not hand-build them.
|
||||
alarm.Create(
|
||||
SystemContext,
|
||||
new NodeId(alarmNodeId, NamespaceIndex),
|
||||
new QualifiedName(displayName, NamespaceIndex),
|
||||
new NodeId(alarmNodeId, ns),
|
||||
new QualifiedName(displayName, ns),
|
||||
new LocalizedText(displayName),
|
||||
assignNodeIds: true);
|
||||
|
||||
@@ -688,7 +757,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// ack to NativeAlarmAckRouter instead of the scripted AlarmCommandRouter. Confirm/AddComment/
|
||||
// Shelve stay on the scripted path even for native conditions (H6c scope is Acknowledge only).
|
||||
alarm.OnAcknowledge = (context, condition, _, comment) =>
|
||||
IsNativeAlarmNode(alarmNodeId)
|
||||
IsNativeAlarmNode(alarmNodeId, realm)
|
||||
? HandleNativeAlarmAck(context, condition, comment)
|
||||
: HandleAlarmCommand(context, condition, "Acknowledge", comment, unshelveAt: null);
|
||||
alarm.OnConfirm = (context, condition, _, comment) =>
|
||||
@@ -736,7 +805,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// engine enable/disable surface — they short-circuit to BadNotSupported.
|
||||
alarm.OnEnableDisable = (context, condition, enabling) =>
|
||||
{
|
||||
if (IsNativeAlarmNode(alarmNodeId))
|
||||
if (IsNativeAlarmNode(alarmNodeId, realm))
|
||||
return new ServiceResult(StatusCodes.BadNotSupported);
|
||||
return HandleAlarmCommand(context, condition, enabling ? "Enable" : "Disable", comment: null, unshelveAt: null);
|
||||
};
|
||||
@@ -749,10 +818,10 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
EnsureFolderIsEventNotifier(parent);
|
||||
|
||||
AddPredefinedNode(SystemContext, alarm);
|
||||
_alarmConditions[alarmNodeId] = alarm;
|
||||
_alarmConditions[conditionKey] = alarm;
|
||||
// H6a: record native (driver-fed) conditions so a later task can route their inbound
|
||||
// Acknowledge to the driver rather than the scripted engine.
|
||||
if (isNative) _nativeAlarmNodeIds.Add(alarmNodeId);
|
||||
if (isNative) _nativeAlarmNodeIds.Add(conditionKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -761,11 +830,11 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// inbound Acknowledge to the driver instead of the scripted engine.</summary>
|
||||
/// <param name="alarmNodeId">The alarm condition node id.</param>
|
||||
/// <returns>True when the condition is native (driver-fed); false when it is scripted or not found.</returns>
|
||||
internal bool IsNativeAlarmNode(string alarmNodeId)
|
||||
internal bool IsNativeAlarmNode(string alarmNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
// _nativeAlarmNodeIds is a plain HashSet mutated only under Lock (in MaterialiseAlarmCondition /
|
||||
// RebuildAddressSpace), so guard the read with the same Lock rather than risk a torn concurrent read.
|
||||
lock (Lock) return _nativeAlarmNodeIds.Contains(alarmNodeId);
|
||||
lock (Lock) return _nativeAlarmNodeIds.Contains(MapKey(realm, alarmNodeId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -906,7 +975,12 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// Capture the optimistic value + the REAL prior value/status BEFORE the SDK applies the write
|
||||
// (at handler entry the node still holds the prior value; returning Good makes the SDK apply `value`).
|
||||
var optimisticValue = value;
|
||||
var nodeKey = node.NodeId.Identifier?.ToString() ?? string.Empty;
|
||||
// v3 dual-namespace: route by the FULL (namespace-qualified) NodeId string so a Raw write and a UNS
|
||||
// write to nodes that share a bare id are distinguishable downstream — WP3's write gateway keys its
|
||||
// (DriverInstanceId, RawPath) resolution on this ns-qualified id. Revert keys off the bare id + realm.
|
||||
var nodeKey = node.NodeId.ToString();
|
||||
var nodeRealm = RealmOf(node.NodeId);
|
||||
var nodeBareId = node.NodeId.Identifier?.ToString() ?? string.Empty;
|
||||
object? priorValue = null;
|
||||
StatusCode priorStatus = StatusCodes.Good;
|
||||
if (node is BaseDataVariableState variable)
|
||||
@@ -931,7 +1005,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
t =>
|
||||
{
|
||||
var outcome = t.IsCompletedSuccessfully ? t.Result : new NodeWriteOutcome(false, "write dispatch faulted");
|
||||
RevertOptimisticWriteIfNeeded(nodeKey, outcome, optimisticValue, priorValue, priorStatus, clientUserId);
|
||||
RevertOptimisticWriteIfNeeded(nodeBareId, outcome, optimisticValue, priorValue, priorStatus, clientUserId, nodeRealm);
|
||||
},
|
||||
CancellationToken.None, TaskContinuationOptions.RunContinuationsAsynchronously, TaskScheduler.Default);
|
||||
|
||||
@@ -1132,14 +1206,15 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// from <see cref="OnEquipmentTagWrite"/> to populate the audit event's ClientUserId; null when unknown.</param>
|
||||
internal void RevertOptimisticWriteIfNeeded(
|
||||
string nodeId, NodeWriteOutcome outcome, object? optimisticValue, object? priorValue, StatusCode priorStatus,
|
||||
string? clientUserId)
|
||||
string? clientUserId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
// Built under Lock if (and only if) a revert is performed, then reported AFTER Lock is released.
|
||||
AuditWriteUpdateEventState? auditEvent = null;
|
||||
|
||||
var key = MapKey(realm, nodeId);
|
||||
lock (Lock)
|
||||
{
|
||||
if (!_variables.TryGetValue(nodeId, out var variable)) return; // rebuilt/removed ⇒ no-op
|
||||
if (!_variables.TryGetValue(key, out var variable)) return; // rebuilt/removed ⇒ no-op
|
||||
if (!ShouldRevert(outcome, variable.Value, optimisticValue)) return; // success, or poll moved it on
|
||||
|
||||
// Item B: surface a transient Bad-quality blip on the still-applied optimistic value, then restore
|
||||
@@ -1292,8 +1367,11 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// A historian is wired: advertise event history on this notifier and register it as a source.
|
||||
// The equipment-folder NodeId identifier IS the equipment id IS the ReadEventsAsync sourceName.
|
||||
folder.EventNotifier = (byte)(folder.EventNotifier | EventNotifiers.HistoryRead);
|
||||
// Key by the full (namespace-qualified) NodeId string so a Raw and a UNS folder that share the
|
||||
// same bare id are distinct sources; the VALUE stays the bare id — that is the historian
|
||||
// ReadEventsAsync sourceName (== the equipment id).
|
||||
var sourceName = folder.NodeId.Identifier?.ToString() ?? string.Empty;
|
||||
_eventNotifierSources[sourceName] = sourceName;
|
||||
_eventNotifierSources[MapKey(folder.NodeId)] = sourceName;
|
||||
}
|
||||
AddRootNotifier(folder);
|
||||
folder.ClearChangeMasks(SystemContext, includeChildren: false);
|
||||
@@ -1320,23 +1398,25 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// <param name="folderNodeId">The node identifier of the folder.</param>
|
||||
/// <param name="parentNodeId">The node identifier of the parent folder; null to use the namespace root.</param>
|
||||
/// <param name="displayName">The display name of the folder.</param>
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName)
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(folderNodeId);
|
||||
ArgumentException.ThrowIfNullOrEmpty(displayName);
|
||||
EnsureAddressSpaceCreated();
|
||||
|
||||
if (_folders.ContainsKey(folderNodeId)) return;
|
||||
var ns = NamespaceIndexForRealm(realm);
|
||||
var key = MapKey(realm, folderNodeId);
|
||||
if (_folders.ContainsKey(key)) return;
|
||||
|
||||
lock (Lock)
|
||||
{
|
||||
if (_folders.ContainsKey(folderNodeId)) return;
|
||||
if (_folders.ContainsKey(key)) return;
|
||||
|
||||
var parent = ResolveParentFolder(parentNodeId);
|
||||
var parent = ResolveParentFolder(parentNodeId, realm);
|
||||
var folder = new FolderState(parent)
|
||||
{
|
||||
NodeId = new NodeId(folderNodeId, NamespaceIndex),
|
||||
BrowseName = new QualifiedName(folderNodeId, NamespaceIndex),
|
||||
NodeId = new NodeId(folderNodeId, ns),
|
||||
BrowseName = new QualifiedName(folderNodeId, ns),
|
||||
DisplayName = displayName,
|
||||
EventNotifier = EventNotifiers.None,
|
||||
TypeDefinitionId = ObjectTypeIds.FolderType,
|
||||
@@ -1344,7 +1424,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
};
|
||||
parent.AddChild(folder);
|
||||
AddPredefinedNode(SystemContext, folder);
|
||||
_folders[folderNodeId] = folder;
|
||||
_folders[key] = folder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1363,13 +1443,13 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// <param name="folderNodeId">The node identifier of the folder to update in place.</param>
|
||||
/// <param name="displayName">The new display name to apply.</param>
|
||||
/// <returns>True when the in-place update was applied; false when the folder id is unknown.</returns>
|
||||
public bool UpdateFolderDisplayName(string folderNodeId, string displayName)
|
||||
public bool UpdateFolderDisplayName(string folderNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(folderNodeId);
|
||||
ArgumentException.ThrowIfNullOrEmpty(displayName);
|
||||
lock (Lock)
|
||||
{
|
||||
if (!_folders.TryGetValue(folderNodeId, out var folder)) return false;
|
||||
if (!_folders.TryGetValue(MapKey(realm, folderNodeId), out var folder)) return false;
|
||||
folder.DisplayName = displayName;
|
||||
folder.ClearChangeMasks(SystemContext, includeChildren: false);
|
||||
return true;
|
||||
@@ -1405,20 +1485,22 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// <paramref name="dataType"/> — rank + dimensions carry the array-ness.</param>
|
||||
/// <param name="arrayLength">Phase 4c: the declared length of the 1-D array when
|
||||
/// <paramref name="isArray"/> is true; ignored for scalars. Null ⇒ length 0.</param>
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null)
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(variableNodeId);
|
||||
ArgumentException.ThrowIfNullOrEmpty(displayName);
|
||||
EnsureAddressSpaceCreated();
|
||||
|
||||
var ns = NamespaceIndexForRealm(realm);
|
||||
var key = MapKey(realm, variableNodeId);
|
||||
// If already present, leave it alone (idempotent re-applies).
|
||||
if (_variables.ContainsKey(variableNodeId)) return;
|
||||
if (_variables.ContainsKey(key)) return;
|
||||
|
||||
lock (Lock)
|
||||
{
|
||||
if (_variables.ContainsKey(variableNodeId)) return;
|
||||
if (_variables.ContainsKey(key)) return;
|
||||
|
||||
var parent = ResolveParentFolder(parentFolderNodeId);
|
||||
var parent = ResolveParentFolder(parentFolderNodeId, realm);
|
||||
// Phase C: a non-null historian tagname makes the node Historizing and grants the HistoryRead
|
||||
// access bit (on top of the writable composite) so clients can browse + HistoryRead it.
|
||||
var historized = historianTagname is not null;
|
||||
@@ -1427,8 +1509,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
var access = ComposeAccessLevel(writable, historized);
|
||||
var variable = new BaseDataVariableState(parent)
|
||||
{
|
||||
NodeId = new NodeId(variableNodeId, NamespaceIndex),
|
||||
BrowseName = new QualifiedName(variableNodeId, NamespaceIndex),
|
||||
NodeId = new NodeId(variableNodeId, ns),
|
||||
BrowseName = new QualifiedName(variableNodeId, ns),
|
||||
DisplayName = displayName,
|
||||
TypeDefinitionId = VariableTypeIds.BaseDataVariableType,
|
||||
ReferenceTypeId = ReferenceTypeIds.Organizes,
|
||||
@@ -1452,10 +1534,11 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
}
|
||||
parent.AddChild(variable);
|
||||
AddPredefinedNode(SystemContext, variable);
|
||||
_variables[variableNodeId] = variable;
|
||||
_variables[key] = variable;
|
||||
// Phase C: register the resolved historian tagname so the HistoryRead override can map this
|
||||
// NodeId back to its Aveva/historian source.
|
||||
if (historized) _historizedTagnames[variableNodeId] = historianTagname!;
|
||||
// NodeId back to its Aveva/historian source. A historized UNS reference node passes the SAME
|
||||
// tagname as its backing raw node — each NodeId keys its own entry, both mapping to one tagname.
|
||||
if (historized) _historizedTagnames[key] = historianTagname!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1500,22 +1583,23 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// <param name="isArray">When true the node becomes a 1-D array (ValueRank=OneDimension); when false scalar.</param>
|
||||
/// <param name="arrayLength">The declared length of the 1-D array when <paramref name="isArray"/> is true; ignored for scalars.</param>
|
||||
/// <returns>True when the in-place update was applied; false when the node id is unknown.</returns>
|
||||
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength)
|
||||
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(variableNodeId);
|
||||
ArgumentException.ThrowIfNullOrEmpty(dataType); // widened surface ⇒ explicit contract (unknown names still map to BaseDataType)
|
||||
var key = MapKey(realm, variableNodeId);
|
||||
BaseDataVariableState? shapeChangedNode = null;
|
||||
lock (Lock)
|
||||
{
|
||||
if (!_variables.TryGetValue(variableNodeId, out var v)) return false;
|
||||
if (!_variables.TryGetValue(key, out var v)) return false;
|
||||
var historized = historianTagname is not null;
|
||||
var access = ComposeAccessLevel(writable, historized);
|
||||
v.AccessLevel = access;
|
||||
v.UserAccessLevel = access;
|
||||
v.Historizing = historized;
|
||||
v.OnWriteValue = writable ? OnEquipmentTagWrite : null;
|
||||
if (historized) _historizedTagnames[variableNodeId] = historianTagname!;
|
||||
else _historizedTagnames.TryRemove(variableNodeId, out _);
|
||||
if (historized) _historizedTagnames[key] = historianTagname!;
|
||||
else _historizedTagnames.TryRemove(key, out _);
|
||||
|
||||
// Swap DataType + ValueRank + ArrayDimensions in place, but only when they actually differ
|
||||
// from the live node — a Writable/Historizing-only change leaves the shape untouched (no value
|
||||
@@ -1628,13 +1712,13 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="affectedNodeId">The folder-scoped node id of the parent under which nodes were added.</param>
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId)
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(affectedNodeId);
|
||||
GeneralModelChangeEventState e;
|
||||
lock (Lock)
|
||||
{
|
||||
e = BuildNodesAddedModelChange(affectedNodeId);
|
||||
e = BuildNodesAddedModelChange(affectedNodeId, realm);
|
||||
}
|
||||
// Report OUTSIDE Lock — Server.ReportEvent re-enters the server's own subscription/event path; holding
|
||||
// Lock across it risks a lock-order inversion (mirrors ReportNodeShapeChangedEvent).
|
||||
@@ -1663,9 +1747,9 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// monitored-item to observe).</summary>
|
||||
/// <param name="affectedNodeId">The folder-scoped node id of the parent under which nodes were added.</param>
|
||||
/// <returns>A populated, unreported <see cref="GeneralModelChangeEventState"/>.</returns>
|
||||
internal GeneralModelChangeEventState BuildNodesAddedModelChange(string affectedNodeId)
|
||||
internal GeneralModelChangeEventState BuildNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
var affected = new NodeId(affectedNodeId, NamespaceIndex);
|
||||
var affected = new NodeId(affectedNodeId, NamespaceIndexForRealm(realm));
|
||||
var e = new GeneralModelChangeEventState(null);
|
||||
e.Initialize(
|
||||
SystemContext,
|
||||
@@ -1683,7 +1767,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// The affected node is the parent the children were added under; carry its TypeDefinition (a Folder
|
||||
// for an equipment parent) just as the shape-changed builder carries the variable's. Null when the
|
||||
// id is unknown — a valid Part 3 "type not applicable", and clients re-browse Affected regardless.
|
||||
AffectedType = ResolveAffectedTypeDefinition(affectedNodeId),
|
||||
AffectedType = ResolveAffectedTypeDefinition(affectedNodeId, realm),
|
||||
Verb = (byte)ModelChangeStructureVerbMask.NodeAdded,
|
||||
};
|
||||
// SetChildValue lazily creates + sets the Changes property (same pattern the audit-event builder
|
||||
@@ -1700,9 +1784,9 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// can assert the populated Changes structure at the nearest deterministic seam.</summary>
|
||||
/// <param name="affectedNodeId">The node id of the deleted node.</param>
|
||||
/// <returns>A populated, unreported <see cref="GeneralModelChangeEventState"/>.</returns>
|
||||
internal GeneralModelChangeEventState BuildNodesRemovedModelChange(string affectedNodeId)
|
||||
internal GeneralModelChangeEventState BuildNodesRemovedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
var affected = new NodeId(affectedNodeId, NamespaceIndex);
|
||||
var affected = new NodeId(affectedNodeId, NamespaceIndexForRealm(realm));
|
||||
var e = new GeneralModelChangeEventState(null);
|
||||
e.Initialize(
|
||||
SystemContext,
|
||||
@@ -1717,7 +1801,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
{
|
||||
Affected = affected,
|
||||
// The node is already gone from the maps, so its TypeDefinition is not applicable (Null).
|
||||
AffectedType = ResolveAffectedTypeDefinition(affectedNodeId),
|
||||
AffectedType = ResolveAffectedTypeDefinition(affectedNodeId, realm),
|
||||
Verb = (byte)ModelChangeStructureVerbMask.NodeDeleted,
|
||||
};
|
||||
e.SetChildValue(SystemContext, BrowseNames.Changes, new[] { change }, false);
|
||||
@@ -1748,10 +1832,11 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// <summary>Resolve the TypeDefinition of a materialised node id from the live folder/variable maps for a
|
||||
/// model-change event's <c>AffectedType</c>; <see cref="NodeId.Null"/> when the id is not registered.</summary>
|
||||
/// <param name="nodeId">The folder-scoped node id whose TypeDefinition is wanted.</param>
|
||||
private NodeId ResolveAffectedTypeDefinition(string nodeId)
|
||||
private NodeId ResolveAffectedTypeDefinition(string nodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
if (_folders.TryGetValue(nodeId, out var folder)) return folder.TypeDefinitionId;
|
||||
if (_variables.TryGetValue(nodeId, out var variable)) return variable.TypeDefinitionId;
|
||||
var key = MapKey(realm, nodeId);
|
||||
if (_folders.TryGetValue(key, out var folder)) return folder.TypeDefinitionId;
|
||||
if (_variables.TryGetValue(key, out var variable)) return variable.TypeDefinitionId;
|
||||
return NodeId.Null;
|
||||
}
|
||||
|
||||
@@ -1828,34 +1913,36 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ISurgicalAddressSpaceSink.RemoveVariableNode"/>
|
||||
public bool RemoveVariableNode(string variableNodeId)
|
||||
public bool RemoveVariableNode(string variableNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(variableNodeId);
|
||||
EnsureAddressSpaceCreated();
|
||||
|
||||
var key = MapKey(realm, variableNodeId);
|
||||
GeneralModelChangeEventState e;
|
||||
lock (Lock)
|
||||
{
|
||||
// Unknown id ⇒ the node-manager maps drifted from what the planner believes; return false so the
|
||||
// caller (AddressSpaceApplier) falls back to a full rebuild (resync). Mirrors the per-node
|
||||
// teardown inside RebuildAddressSpace, scoped to this one id.
|
||||
if (!_variables.TryRemove(variableNodeId, out var variable)) return false;
|
||||
if (!_variables.TryRemove(key, out var variable)) return false;
|
||||
variable.Parent?.RemoveChild(variable);
|
||||
PredefinedNodes?.Remove(variable.NodeId);
|
||||
// Drop the historized-tagname registration alongside the variable it maps (Phase C parity).
|
||||
_historizedTagnames.TryRemove(variableNodeId, out _);
|
||||
e = BuildNodesRemovedModelChange(variableNodeId);
|
||||
_historizedTagnames.TryRemove(key, out _);
|
||||
e = BuildNodesRemovedModelChange(variableNodeId, realm);
|
||||
}
|
||||
ReportModelChangeOutsideLock(e, variableNodeId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ISurgicalAddressSpaceSink.RemoveAlarmConditionNode"/>
|
||||
public bool RemoveAlarmConditionNode(string alarmNodeId)
|
||||
public bool RemoveAlarmConditionNode(string alarmNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(alarmNodeId);
|
||||
EnsureAddressSpaceCreated();
|
||||
|
||||
var key = MapKey(realm, alarmNodeId);
|
||||
GeneralModelChangeEventState e;
|
||||
lock (Lock)
|
||||
{
|
||||
@@ -1864,33 +1951,36 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// promotion is intentionally NOT demoted here — other conditions may still hang under it, and a
|
||||
// lingering notifier folder is harmless (RemoveEquipmentSubtree demotes it when the whole
|
||||
// equipment goes).
|
||||
if (!_alarmConditions.TryRemove(alarmNodeId, out var condition)) return false;
|
||||
if (!_alarmConditions.TryRemove(key, out var condition)) return false;
|
||||
condition.Parent?.RemoveChild(condition);
|
||||
PredefinedNodes?.Remove(condition.NodeId);
|
||||
_nativeAlarmNodeIds.Remove(alarmNodeId);
|
||||
e = BuildNodesRemovedModelChange(alarmNodeId);
|
||||
_nativeAlarmNodeIds.Remove(key);
|
||||
e = BuildNodesRemovedModelChange(alarmNodeId, realm);
|
||||
}
|
||||
ReportModelChangeOutsideLock(e, alarmNodeId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ISurgicalAddressSpaceSink.RemoveEquipmentSubtree"/>
|
||||
public bool RemoveEquipmentSubtree(string equipmentNodeId)
|
||||
public bool RemoveEquipmentSubtree(string equipmentNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(equipmentNodeId);
|
||||
EnsureAddressSpaceCreated();
|
||||
|
||||
var equipKey = MapKey(realm, equipmentNodeId);
|
||||
GeneralModelChangeEventState e;
|
||||
lock (Lock)
|
||||
{
|
||||
// Unknown equipment id ⇒ map drift ⇒ false (caller rebuilds).
|
||||
if (!_folders.ContainsKey(equipmentNodeId)) return false;
|
||||
if (!_folders.ContainsKey(equipKey)) return false;
|
||||
|
||||
// Folder-scoped NodeIds mean every descendant's id is the equipment id itself or begins with
|
||||
// "<equipmentId>/" (sub-folders, variables, condition nodes). Snapshot each map's in-scope keys
|
||||
// first (ToList) so we don't mutate while enumerating.
|
||||
var prefix = equipmentNodeId + "/";
|
||||
bool InScope(string id) => id == equipmentNodeId || id.StartsWith(prefix, StringComparison.Ordinal);
|
||||
// Maps key on the full namespace-qualified NodeId string (e.g. "ns=3;s=<equipmentId>"). Every
|
||||
// descendant in the SAME realm shares that key exactly or begins with "<equipKey>/" (sub-folders,
|
||||
// variables, condition nodes); the "ns=N;s=" prefix makes the match realm-scoped for free, so a
|
||||
// node in the OTHER namespace that happens to share a bare id is never in scope. Snapshot each
|
||||
// map's in-scope keys first (ToList) so we don't mutate while enumerating.
|
||||
var prefix = equipKey + "/";
|
||||
bool InScope(string key) => key == equipKey || key.StartsWith(prefix, StringComparison.Ordinal);
|
||||
|
||||
// Value variables (+ their historized-tagname registrations).
|
||||
foreach (var id in _variables.Keys.Where(InScope).ToList())
|
||||
@@ -1917,8 +2007,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// Notifier demotion BEFORE dropping the folders: sever the Server↔folder HasNotifier ref for
|
||||
// every promoted folder in the subtree (an equipment folder, or a sub-folder that hosted an
|
||||
// alarm), else the removal leaks an orphaned root-notifier reference on the Server object.
|
||||
// _notifierFolders is keyed by the folder's NodeId; match on its string identifier.
|
||||
foreach (var kvp in _notifierFolders.Where(kv => InScope(kv.Key.Identifier?.ToString() ?? string.Empty)).ToList())
|
||||
// _notifierFolders is keyed by the folder's NodeId; match InScope on its full (ns-qualified) string.
|
||||
foreach (var kvp in _notifierFolders.Where(kv => InScope(kv.Key.ToString())).ToList())
|
||||
{
|
||||
RemoveRootNotifier(kvp.Value);
|
||||
_notifierFolders.Remove(kvp.Key);
|
||||
@@ -1937,17 +2027,19 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
}
|
||||
}
|
||||
|
||||
e = BuildNodesRemovedModelChange(equipmentNodeId);
|
||||
e = BuildNodesRemovedModelChange(equipmentNodeId, realm);
|
||||
}
|
||||
ReportModelChangeOutsideLock(e, equipmentNodeId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private FolderState ResolveParentFolder(string? parentNodeId)
|
||||
private FolderState ResolveParentFolder(string? parentNodeId, AddressSpaceRealm realm)
|
||||
{
|
||||
EnsureAddressSpaceCreated();
|
||||
if (string.IsNullOrEmpty(parentNodeId)) return _root!;
|
||||
return _folders.TryGetValue(parentNodeId, out var existing) ? existing : _root!;
|
||||
// A node's parent lives in the SAME realm (a raw tag's group is raw; a UNS reference's equipment
|
||||
// folder is UNS), so resolve the parent key in that realm; unknown parent ⇒ hang under the shared root.
|
||||
return _folders.TryGetValue(MapKey(realm, parentNodeId), out var existing) ? existing : _root!;
|
||||
}
|
||||
|
||||
/// <summary>Guard the address-space mutators against a too-early call. <c>_root</c>
|
||||
@@ -2124,8 +2216,9 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
var work = new List<Func<Task>>(nodesToProcess.Count);
|
||||
foreach (var handle in nodesToProcess)
|
||||
{
|
||||
var idString = handle.NodeId.Identifier?.ToString();
|
||||
if (idString is null || !_eventNotifierSources.TryGetValue(idString, out var sourceName))
|
||||
// Key on the full (namespace-qualified) NodeId so a Raw and a UNS notifier folder that share a
|
||||
// bare id resolve to their own source registration.
|
||||
if (!_eventNotifierSources.TryGetValue(MapKey(handle.NodeId), out var sourceName))
|
||||
{
|
||||
// Not a registered event-history source (plain folder / Null-source promotion) ⇒ unsupported.
|
||||
// Set both errors and results explicitly on every bad path — don't rely on the SDK base
|
||||
@@ -2438,8 +2531,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
Func<IHistorianDataSource, string, CancellationToken, Task<HistorianRead>> read,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var idString = handle.NodeId.Identifier?.ToString();
|
||||
if (idString is null || !TryGetHistorizedTagname(idString, out var tagname))
|
||||
if (!TryGetHistorizedTagname(handle.NodeId, out var tagname))
|
||||
{
|
||||
// Not a historized node we own a tagname for — unsupported. (The base pre-seeds this same
|
||||
// status, but set it explicitly so the contract is local + obvious.)
|
||||
@@ -2557,8 +2649,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
else
|
||||
{
|
||||
// Fresh read: resolve the node's historian tagname (as ServeNode does).
|
||||
var idString = handle.NodeId.Identifier?.ToString();
|
||||
if (idString is null || !TryGetHistorizedTagname(idString, out var resolved) || resolved is null)
|
||||
if (!TryGetHistorizedTagname(handle.NodeId, out var resolved) || resolved is null)
|
||||
{
|
||||
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
|
||||
return;
|
||||
@@ -2800,12 +2891,12 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
}
|
||||
}
|
||||
|
||||
private BaseDataVariableState CreateVariable(string nodeId)
|
||||
private BaseDataVariableState CreateVariable(string nodeId, ushort ns)
|
||||
{
|
||||
var v = new BaseDataVariableState(_root)
|
||||
{
|
||||
NodeId = new NodeId(nodeId, NamespaceIndex),
|
||||
BrowseName = new QualifiedName(nodeId, NamespaceIndex),
|
||||
NodeId = new NodeId(nodeId, ns),
|
||||
BrowseName = new QualifiedName(nodeId, ns),
|
||||
DisplayName = nodeId,
|
||||
TypeDefinitionId = VariableTypeIds.BaseDataVariableType,
|
||||
ReferenceTypeId = ReferenceTypeIds.Organizes,
|
||||
|
||||
@@ -21,48 +21,48 @@ public sealed class SdkAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgicalAddre
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc)
|
||||
=> _nodeManager.WriteValue(nodeId, value, quality, sourceTimestampUtc);
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _nodeManager.WriteValue(nodeId, value, quality, sourceTimestampUtc, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc)
|
||||
=> _nodeManager.WriteAlarmCondition(alarmNodeId, state, sourceTimestampUtc);
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _nodeManager.WriteAlarmCondition(alarmNodeId, state, sourceTimestampUtc, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false)
|
||||
=> _nodeManager.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative);
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _nodeManager.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName)
|
||||
=> _nodeManager.EnsureFolder(folderNodeId, parentNodeId, displayName);
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _nodeManager.EnsureFolder(folderNodeId, parentNodeId, displayName, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null)
|
||||
=> _nodeManager.EnsureVariable(variableNodeId, parentFolderNodeId, displayName, dataType, writable, historianTagname, isArray, arrayLength);
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _nodeManager.EnsureVariable(variableNodeId, parentFolderNodeId, displayName, dataType, writable, historianTagname, isArray, arrayLength, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength)
|
||||
=> _nodeManager.UpdateTagAttributes(variableNodeId, writable, historianTagname, dataType, isArray, arrayLength);
|
||||
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _nodeManager.UpdateTagAttributes(variableNodeId, writable, historianTagname, dataType, isArray, arrayLength, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool UpdateFolderDisplayName(string folderNodeId, string displayName)
|
||||
=> _nodeManager.UpdateFolderDisplayName(folderNodeId, displayName);
|
||||
public bool UpdateFolderDisplayName(string folderNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _nodeManager.UpdateFolderDisplayName(folderNodeId, displayName, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool RemoveVariableNode(string variableNodeId)
|
||||
=> _nodeManager.RemoveVariableNode(variableNodeId);
|
||||
public bool RemoveVariableNode(string variableNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _nodeManager.RemoveVariableNode(variableNodeId, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool RemoveAlarmConditionNode(string alarmNodeId)
|
||||
=> _nodeManager.RemoveAlarmConditionNode(alarmNodeId);
|
||||
public bool RemoveAlarmConditionNode(string alarmNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _nodeManager.RemoveAlarmConditionNode(alarmNodeId, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool RemoveEquipmentSubtree(string equipmentNodeId)
|
||||
=> _nodeManager.RemoveEquipmentSubtree(equipmentNodeId);
|
||||
public bool RemoveEquipmentSubtree(string equipmentNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _nodeManager.RemoveEquipmentSubtree(equipmentNodeId, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RebuildAddressSpace() => _nodeManager.RebuildAddressSpace();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId) => _nodeManager.RaiseNodesAddedModelChange(affectedNodeId);
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => _nodeManager.RaiseNodesAddedModelChange(affectedNodeId, realm);
|
||||
}
|
||||
|
||||
@@ -186,15 +186,15 @@ public class DeferredAddressSpaceSinkTests
|
||||
public bool WriteValueCalled { get; private set; }
|
||||
public bool RebuildCalled { get; private set; }
|
||||
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc)
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> WriteValueCalled = true;
|
||||
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false) { }
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) { }
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void RebuildAddressSpace() => RebuildCalled = true;
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId) { }
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
}
|
||||
|
||||
private sealed class SpySurgicalSink : IOpcUaAddressSpaceSink, ISurgicalAddressSpaceSink
|
||||
@@ -202,21 +202,21 @@ public class DeferredAddressSpaceSinkTests
|
||||
public bool UpdateCalled { get; private set; }
|
||||
public bool FolderRenameCalled { get; private set; }
|
||||
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false) { }
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) { }
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null) { }
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void RebuildAddressSpace() { }
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId) { }
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
|
||||
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength)
|
||||
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
UpdateCalled = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool UpdateFolderDisplayName(string folderNodeId, string displayName)
|
||||
public bool UpdateFolderDisplayName(string folderNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
FolderRenameCalled = true;
|
||||
return true;
|
||||
@@ -226,8 +226,8 @@ public class DeferredAddressSpaceSinkTests
|
||||
public bool RemoveAlarmCalled { get; private set; }
|
||||
public bool RemoveSubtreeCalled { get; private set; }
|
||||
|
||||
public bool RemoveVariableNode(string variableNodeId) { RemoveVariableCalled = true; return true; }
|
||||
public bool RemoveAlarmConditionNode(string alarmNodeId) { RemoveAlarmCalled = true; return true; }
|
||||
public bool RemoveEquipmentSubtree(string equipmentNodeId) { RemoveSubtreeCalled = true; return true; }
|
||||
public bool RemoveVariableNode(string variableNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { RemoveVariableCalled = true; return true; }
|
||||
public bool RemoveAlarmConditionNode(string alarmNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { RemoveAlarmCalled = true; return true; }
|
||||
public bool RemoveEquipmentSubtree(string equipmentNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { RemoveSubtreeCalled = true; return true; }
|
||||
}
|
||||
}
|
||||
|
||||
+18
@@ -74,6 +74,24 @@ public class DeferredSinkForwardingReflectionTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Every_node_naming_sink_method_carries_a_realm_discriminator()
|
||||
{
|
||||
// v3 B4-WP2 dual-namespace invariant: a node id alone is no longer globally unique across the two
|
||||
// namespaces (Raw vs UNS), so every sink method that NAMES a node must carry an AddressSpaceRealm
|
||||
// discriminator — the sink resolves the namespace from the realm rather than parsing the id string.
|
||||
// RebuildAddressSpace is the sole node-naming-free method (it clears BOTH realms). This guard locks in
|
||||
// the realm surface so a future method can't be added that names a node without a realm.
|
||||
foreach (var method in ForwardingInterfaces.SelectMany(i => i.GetMethods()))
|
||||
{
|
||||
if (method.Name == nameof(IOpcUaAddressSpaceSink.RebuildAddressSpace)) continue;
|
||||
|
||||
method.GetParameters().Any(p => p.ParameterType == typeof(AddressSpaceRealm)).ShouldBeTrue(
|
||||
$"{method.DeclaringType!.Name}.{method.Name} names a node but has no AddressSpaceRealm parameter — " +
|
||||
"a bare node id is ambiguous across the Raw and UNS namespaces (B4-WP2). Add the realm discriminator.");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Every_IServiceLevelPublisher_method_reaches_the_inner_publisher()
|
||||
{
|
||||
|
||||
+6
-6
@@ -161,24 +161,24 @@ public sealed class AddressSpaceApplierFailureSurfaceTests
|
||||
public bool ThrowOnAlarmWrite { get; init; }
|
||||
public bool ThrowOnMaterialiseAlarm { get; init; }
|
||||
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc) { }
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc)
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
if (ThrowOnAlarmWrite) throw new InvalidOperationException("simulated WriteAlarmCondition fault");
|
||||
}
|
||||
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false)
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
if (ThrowOnMaterialiseAlarm) throw new InvalidOperationException("simulated MaterialiseAlarmCondition fault");
|
||||
}
|
||||
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName)
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
if (ThrowOnEnsureFolder) throw new InvalidOperationException("simulated EnsureFolder fault");
|
||||
}
|
||||
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null)
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
if (ThrowOnEnsureVariable) throw new InvalidOperationException("simulated EnsureVariable fault");
|
||||
}
|
||||
@@ -188,6 +188,6 @@ public sealed class AddressSpaceApplierFailureSurfaceTests
|
||||
if (ThrowOnRebuild) throw new InvalidOperationException("simulated RebuildAddressSpace fault");
|
||||
}
|
||||
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId) { }
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -307,24 +307,24 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="quality">The OPC UA quality value.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc) { }
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>Records an alarm condition write (stub implementation for testing).</summary>
|
||||
/// <param name="alarmNodeId">The node ID of the alarm condition.</param>
|
||||
/// <param name="state">The full condition state snapshot.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>Materialises an alarm condition (stub implementation for testing).</summary>
|
||||
/// <param name="alarmNodeId">The alarm node ID (== ScriptedAlarmId).</param>
|
||||
/// <param name="equipmentNodeId">The equipment folder node ID.</param>
|
||||
/// <param name="displayName">The condition display name.</param>
|
||||
/// <param name="alarmType">The domain alarm type.</param>
|
||||
/// <param name="severity">The domain severity.</param>
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>Records a folder creation request.</summary>
|
||||
/// <param name="folderNodeId">The node ID of the folder.</param>
|
||||
/// <param name="parentNodeId">The node ID of the parent folder, or null for root.</param>
|
||||
/// <param name="displayName">The display name of the folder.</param>
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName)
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _calls.Enqueue((folderNodeId, parentNodeId, displayName));
|
||||
/// <summary>Ensures a variable exists (stub implementation for testing).</summary>
|
||||
/// <param name="variableNodeId">The node ID of the variable.</param>
|
||||
@@ -333,10 +333,10 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable
|
||||
/// <param name="dataType">The OPC UA built-in type name.</param>
|
||||
/// <param name="writable">Whether the node is created read/write.</param>
|
||||
/// <param name="historianTagname">The resolved historian tagname (null ⇒ not historized).</param>
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null) { }
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>Rebuilds the address space (stub implementation for testing).</summary>
|
||||
public void RebuildAddressSpace() { }
|
||||
/// <summary>Announces a NodeAdded model-change (stub implementation for testing).</summary>
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId) { }
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2170,7 +2170,7 @@ public sealed class AddressSpaceApplierTests
|
||||
/// <param name="dataType">The new OPC UA data type name to apply in place.</param>
|
||||
/// <param name="isArray">The new array-ness of the node.</param>
|
||||
/// <param name="arrayLength">The new 1-D array length when <paramref name="isArray"/> is true.</param>
|
||||
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength)
|
||||
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
SurgicalQueue.Enqueue((variableNodeId, writable, historianTagname, dataType, isArray, arrayLength));
|
||||
return SurgicalReturns;
|
||||
@@ -2187,7 +2187,7 @@ public sealed class AddressSpaceApplierTests
|
||||
/// <summary>Records a surgical in-place folder display-name update; returns <see cref="FolderRenameReturns"/>.</summary>
|
||||
/// <param name="folderNodeId">The folder node ID to update in place.</param>
|
||||
/// <param name="displayName">The new display name to apply.</param>
|
||||
public bool UpdateFolderDisplayName(string folderNodeId, string displayName)
|
||||
public bool UpdateFolderDisplayName(string folderNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
FolderRenameQueue.Enqueue((folderNodeId, displayName));
|
||||
return FolderRenameReturns;
|
||||
@@ -2202,21 +2202,21 @@ public sealed class AddressSpaceApplierTests
|
||||
public bool RemoveReturns { get; init; } = true;
|
||||
|
||||
/// <summary>Records a surgical variable-node removal; returns <see cref="RemoveReturns"/>.</summary>
|
||||
public bool RemoveVariableNode(string variableNodeId)
|
||||
public bool RemoveVariableNode(string variableNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
RemoveQueue.Enqueue(("var", variableNodeId));
|
||||
return RemoveReturns;
|
||||
}
|
||||
|
||||
/// <summary>Records a surgical alarm-condition-node removal; returns <see cref="RemoveReturns"/>.</summary>
|
||||
public bool RemoveAlarmConditionNode(string alarmNodeId)
|
||||
public bool RemoveAlarmConditionNode(string alarmNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
RemoveQueue.Enqueue(("alarm", alarmNodeId));
|
||||
return RemoveReturns;
|
||||
}
|
||||
|
||||
/// <summary>Records a surgical equipment-subtree removal; returns <see cref="RemoveReturns"/>.</summary>
|
||||
public bool RemoveEquipmentSubtree(string equipmentNodeId)
|
||||
public bool RemoveEquipmentSubtree(string equipmentNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
RemoveQueue.Enqueue(("equipment", equipmentNodeId));
|
||||
return RemoveReturns;
|
||||
@@ -2262,13 +2262,13 @@ public sealed class AddressSpaceApplierTests
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="quality">The OPC UA quality.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc)
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> ValueWriteQueue.Enqueue((nodeId, quality));
|
||||
/// <summary>Records an alarm condition write call.</summary>
|
||||
/// <param name="alarmNodeId">The alarm node ID.</param>
|
||||
/// <param name="state">The full condition state snapshot.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc)
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> AlarmQueue.Enqueue((alarmNodeId, state));
|
||||
/// <summary>Records an alarm-condition materialise call.</summary>
|
||||
/// <param name="alarmNodeId">The alarm node ID (== ScriptedAlarmId).</param>
|
||||
@@ -2276,13 +2276,13 @@ public sealed class AddressSpaceApplierTests
|
||||
/// <param name="displayName">The condition display name.</param>
|
||||
/// <param name="alarmType">The domain alarm type.</param>
|
||||
/// <param name="severity">The domain severity.</param>
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false)
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> AlarmConditionQueue.Enqueue((alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative));
|
||||
/// <summary>Records a folder creation call.</summary>
|
||||
/// <param name="folderNodeId">The folder node ID.</param>
|
||||
/// <param name="parentNodeId">The parent folder node ID, if any.</param>
|
||||
/// <param name="displayName">The display name for the folder.</param>
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName)
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> FolderQueue.Enqueue((folderNodeId, parentNodeId, displayName));
|
||||
/// <summary>Records a variable creation call.</summary>
|
||||
/// <param name="variableNodeId">The variable node ID.</param>
|
||||
@@ -2291,7 +2291,7 @@ public sealed class AddressSpaceApplierTests
|
||||
/// <param name="dataType">The OPC UA built-in type name.</param>
|
||||
/// <param name="writable">Whether the node is created read/write.</param>
|
||||
/// <param name="historianTagname">The resolved historian tagname (null ⇒ not historized).</param>
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null)
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
VariableQueue.Enqueue((variableNodeId, parentFolderNodeId, displayName, dataType, writable));
|
||||
HistorianQueue.Enqueue((variableNodeId, historianTagname));
|
||||
@@ -2306,7 +2306,7 @@ public sealed class AddressSpaceApplierTests
|
||||
public List<string> ModelChangeCalls => ModelChangeQueue.ToList();
|
||||
/// <summary>Records a NodeAdded model-change announcement.</summary>
|
||||
/// <param name="affectedNodeId">The node under which discovered nodes were added.</param>
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId) => ModelChangeQueue.Enqueue(affectedNodeId);
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => ModelChangeQueue.Enqueue(affectedNodeId);
|
||||
}
|
||||
|
||||
/// <summary>A recording sink that does NOT implement <see cref="ISurgicalAddressSpaceSink"/> — used to
|
||||
@@ -2318,19 +2318,19 @@ public sealed class AddressSpaceApplierTests
|
||||
public int RebuildCalls;
|
||||
|
||||
/// <summary>Records a value write (no-op in this sink).</summary>
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc) { }
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>No-op alarm condition write call.</summary>
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>No-op alarm-condition materialise call.</summary>
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>No-op folder creation call.</summary>
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) { }
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>No-op variable creation call.</summary>
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null) { }
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>Records a rebuild address space call.</summary>
|
||||
public void RebuildAddressSpace() => Interlocked.Increment(ref RebuildCalls);
|
||||
/// <summary>No-op NodeAdded model-change announcement.</summary>
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId) { }
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
}
|
||||
|
||||
private sealed class ThrowingSink : IOpcUaAddressSpaceSink
|
||||
@@ -2345,13 +2345,13 @@ public sealed class AddressSpaceApplierTests
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="quality">The OPC UA quality.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc) { }
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>Throws an exception if configured to do so.</summary>
|
||||
/// <param name="alarmNodeId">The alarm node ID.</param>
|
||||
/// <param name="state">The full condition state snapshot.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when configured to throw on alarm write.</exception>
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc)
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
if (_throwOnAlarmWrite) throw new InvalidOperationException("simulated sink fault");
|
||||
}
|
||||
@@ -2361,12 +2361,12 @@ public sealed class AddressSpaceApplierTests
|
||||
/// <param name="displayName">The condition display name.</param>
|
||||
/// <param name="alarmType">The domain alarm type.</param>
|
||||
/// <param name="severity">The domain severity.</param>
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>No-op folder creation call.</summary>
|
||||
/// <param name="folderNodeId">The folder node ID.</param>
|
||||
/// <param name="parentNodeId">The parent folder node ID, if any.</param>
|
||||
/// <param name="displayName">The display name for the folder.</param>
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) { }
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>No-op variable creation call.</summary>
|
||||
/// <param name="variableNodeId">The variable node ID.</param>
|
||||
/// <param name="parentFolderNodeId">The parent folder node ID, if any.</param>
|
||||
@@ -2374,10 +2374,10 @@ public sealed class AddressSpaceApplierTests
|
||||
/// <param name="dataType">The OPC UA built-in type name.</param>
|
||||
/// <param name="writable">Whether the node is created read/write.</param>
|
||||
/// <param name="historianTagname">The resolved historian tagname (null ⇒ not historized).</param>
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null) { }
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>No-op rebuild address space call.</summary>
|
||||
public void RebuildAddressSpace() { }
|
||||
/// <summary>No-op NodeAdded model-change announcement.</summary>
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId) { }
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,19 +228,19 @@ public sealed class DeferredAddressSpaceSinkTests
|
||||
public List<(string NodeId, string? HistorianTagname)> HistorianCalls => HistorianQueue.ToList();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc)
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> CallQueue.Enqueue($"WV:{nodeId}");
|
||||
/// <inheritdoc />
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc)
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> CallQueue.Enqueue($"WA:{alarmNodeId}");
|
||||
/// <inheritdoc />
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false)
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> CallQueue.Enqueue($"MA:{alarmNodeId}");
|
||||
/// <inheritdoc />
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName)
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> CallQueue.Enqueue($"EF:{folderNodeId}");
|
||||
/// <inheritdoc />
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null)
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
CallQueue.Enqueue($"EV:{variableNodeId}");
|
||||
HistorianQueue.Enqueue((variableNodeId, historianTagname));
|
||||
@@ -248,7 +248,7 @@ public sealed class DeferredAddressSpaceSinkTests
|
||||
/// <inheritdoc />
|
||||
public void RebuildAddressSpace() => CallQueue.Enqueue("RB");
|
||||
/// <inheritdoc />
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId) => CallQueue.Enqueue($"NA:{affectedNodeId}");
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => CallQueue.Enqueue($"NA:{affectedNodeId}");
|
||||
}
|
||||
|
||||
private sealed class SurgicalRecordingSink : IOpcUaAddressSpaceSink, ISurgicalAddressSpaceSink
|
||||
@@ -261,14 +261,14 @@ public sealed class DeferredAddressSpaceSinkTests
|
||||
public List<(string FolderNodeId, string DisplayName)> FolderRenameCalls { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength)
|
||||
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
SurgicalCalls.Add((variableNodeId, writable, historianTagname, dataType, isArray, arrayLength));
|
||||
return Result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool UpdateFolderDisplayName(string folderNodeId, string displayName)
|
||||
public bool UpdateFolderDisplayName(string folderNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
FolderRenameCalls.Add((folderNodeId, displayName));
|
||||
return Result;
|
||||
@@ -278,25 +278,25 @@ public sealed class DeferredAddressSpaceSinkTests
|
||||
public List<(string Kind, string NodeId)> RemoveCalls { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool RemoveVariableNode(string variableNodeId) { RemoveCalls.Add(("var", variableNodeId)); return Result; }
|
||||
public bool RemoveVariableNode(string variableNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { RemoveCalls.Add(("var", variableNodeId)); return Result; }
|
||||
/// <inheritdoc />
|
||||
public bool RemoveAlarmConditionNode(string alarmNodeId) { RemoveCalls.Add(("alarm", alarmNodeId)); return Result; }
|
||||
public bool RemoveAlarmConditionNode(string alarmNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { RemoveCalls.Add(("alarm", alarmNodeId)); return Result; }
|
||||
/// <inheritdoc />
|
||||
public bool RemoveEquipmentSubtree(string equipmentNodeId) { RemoveCalls.Add(("equipment", equipmentNodeId)); return Result; }
|
||||
public bool RemoveEquipmentSubtree(string equipmentNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { RemoveCalls.Add(("equipment", equipmentNodeId)); return Result; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc) { }
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <inheritdoc />
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <inheritdoc />
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <inheritdoc />
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) { }
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <inheritdoc />
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null) { }
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <inheritdoc />
|
||||
public void RebuildAddressSpace() { }
|
||||
/// <inheritdoc />
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId) { }
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,11 +111,14 @@ public sealed class SdkAddressSpaceSinkTests : IDisposable
|
||||
condition.ShouldNotBeNull();
|
||||
// It is a REAL Part 9 alarm condition (subtype mapped from "OffNormalAlarm").
|
||||
condition.ShouldBeOfType<OffNormalAlarmState>();
|
||||
condition.NodeId.ShouldBe(new NodeId("alm-1", nm.NamespaceIndex));
|
||||
// v3 dual-namespace: the sink's transitional default realm is UNS, so default-realm nodes live in the
|
||||
// UNS namespace (index 3), not the Raw namespace nm.NamespaceIndex (index 2) reports.
|
||||
var unsNs = nm.NamespaceIndexForRealm(AddressSpaceRealm.Uns);
|
||||
condition.NodeId.ShouldBe(new NodeId("alm-1", unsNs));
|
||||
|
||||
// Reachable under the equipment folder: the parent is the eq-1 folder (HasComponent child).
|
||||
condition.Parent.ShouldNotBeNull();
|
||||
condition.Parent!.NodeId.ShouldBe(new NodeId("eq-1", nm.NamespaceIndex));
|
||||
condition.Parent!.NodeId.ShouldBe(new NodeId("eq-1", unsNs));
|
||||
|
||||
// Initial state set by MaterialiseAlarmCondition: enabled, inactive, acked, retain=false.
|
||||
condition.EnabledState.Id.Value.ShouldBeTrue();
|
||||
|
||||
+6
-6
@@ -324,27 +324,27 @@ public sealed class DiscoveryInjectionEndToEndTests : RuntimeActorTestBase
|
||||
public int RebuildCalls;
|
||||
|
||||
/// <summary>Records a live-value write.</summary>
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc)
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _values.Enqueue((nodeId, value, quality, sourceTimestampUtc));
|
||||
|
||||
/// <summary>No-op: alarm writes are not exercised by this suite.</summary>
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
|
||||
/// <summary>No-op: alarm materialise is not exercised by this suite.</summary>
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
|
||||
/// <summary>Records an EnsureFolder call.</summary>
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName)
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _folders.Enqueue((folderNodeId, parentNodeId, displayName));
|
||||
|
||||
/// <summary>Records an EnsureVariable call.</summary>
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null)
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> _variables.Enqueue((variableNodeId, parentFolderNodeId, displayName, dataType, writable));
|
||||
|
||||
/// <summary>Records a raw rebuild (the apply-time fallback when no dbFactory is wired).</summary>
|
||||
public void RebuildAddressSpace() => Interlocked.Increment(ref RebuildCalls);
|
||||
|
||||
/// <summary>Records a NodeAdded model-change announcement.</summary>
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId) => _modelChanges.Enqueue(affectedNodeId);
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => _modelChanges.Enqueue(affectedNodeId);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -194,24 +194,24 @@ public sealed class OtOpcUaTelemetryHookTests : RuntimeActorTestBase
|
||||
/// <param name="value">The value being written.</param>
|
||||
/// <param name="quality">The OPC UA quality status.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc) => Writes++;
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => Writes++;
|
||||
/// <summary>Records an alarm condition write.</summary>
|
||||
/// <param name="alarmNodeId">The alarm node identifier.</param>
|
||||
/// <param name="state">The full condition state snapshot.</param>
|
||||
/// <param name="occurredUtc">The time the alarm occurred in UTC.</param>
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime occurredUtc) => Writes++;
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime occurredUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => Writes++;
|
||||
/// <summary>Materialises an alarm condition (stub implementation).</summary>
|
||||
/// <param name="alarmNodeId">The alarm node identifier.</param>
|
||||
/// <param name="equipmentNodeId">The equipment folder node identifier.</param>
|
||||
/// <param name="displayName">The condition display name.</param>
|
||||
/// <param name="alarmType">The domain alarm type.</param>
|
||||
/// <param name="severity">The domain severity.</param>
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>Ensures folder exists (stub implementation).</summary>
|
||||
/// <param name="folderNodeId">The folder node identifier.</param>
|
||||
/// <param name="parentNodeId">The parent folder node identifier.</param>
|
||||
/// <param name="displayName">The display name for the folder.</param>
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) { }
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>Ensures variable exists (stub implementation).</summary>
|
||||
/// <param name="variableNodeId">The variable node identifier.</param>
|
||||
/// <param name="parentFolderNodeId">The parent folder node identifier.</param>
|
||||
@@ -219,10 +219,10 @@ public sealed class OtOpcUaTelemetryHookTests : RuntimeActorTestBase
|
||||
/// <param name="dataType">The OPC UA built-in type name.</param>
|
||||
/// <param name="writable">Whether the node is created read/write.</param>
|
||||
/// <param name="historianTagname">The resolved historian tagname (null ⇒ not historized).</param>
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null) { }
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>Rebuilds address space (recorded via span).</summary>
|
||||
public void RebuildAddressSpace() { /* recorded via span */ }
|
||||
/// <summary>Announces a NodeAdded model-change (stub implementation).</summary>
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId) { }
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
}
|
||||
}
|
||||
|
||||
+12
-12
@@ -118,25 +118,25 @@ public sealed class OpcUaPublishActorApplyFailureTests : RuntimeActorTestBase
|
||||
/// <summary>A sink whose RebuildAddressSpace throws (drives the applier's SafeRebuild catch → RebuildFailed).</summary>
|
||||
private sealed class ThrowOnRebuildSink : IOpcUaAddressSpaceSink
|
||||
{
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime occurredUtc) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false) { }
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) { }
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null) { }
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime occurredUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void RebuildAddressSpace() => throw new InvalidOperationException("simulated rebuild fault");
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId) { }
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
}
|
||||
|
||||
/// <summary>A no-op sink — a clean apply (no failures).</summary>
|
||||
private sealed class NoopSink : IOpcUaAddressSpaceSink
|
||||
{
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime occurredUtc) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false) { }
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) { }
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null) { }
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime occurredUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void RebuildAddressSpace() { }
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId) { }
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
}
|
||||
|
||||
/// <summary>Listens to a single instrument by name on the central meter and tallies value + tags.</summary>
|
||||
|
||||
+11
-11
@@ -437,13 +437,13 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="quality">The OPC UA quality code.</param>
|
||||
/// <param name="ts">The timestamp of the write.</param>
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime ts)
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime ts, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> Calls.Enqueue($"WV:{nodeId}");
|
||||
/// <summary>Records an alarm condition write call.</summary>
|
||||
/// <param name="alarmNodeId">The alarm node ID.</param>
|
||||
/// <param name="state">The full condition state snapshot.</param>
|
||||
/// <param name="ts">The timestamp of the state change.</param>
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime ts)
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime ts, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> Calls.Enqueue($"WA:{alarmNodeId}");
|
||||
/// <summary>Records a materialise-alarm-condition call.</summary>
|
||||
/// <param name="alarmNodeId">The alarm node ID (== ScriptedAlarmId).</param>
|
||||
@@ -451,13 +451,13 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
/// <param name="displayName">The condition display name.</param>
|
||||
/// <param name="alarmType">The domain alarm type.</param>
|
||||
/// <param name="severity">The domain severity.</param>
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false)
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> Calls.Enqueue($"MA:{alarmNodeId}");
|
||||
/// <summary>Records a folder ensure call.</summary>
|
||||
/// <param name="folderNodeId">The folder node ID.</param>
|
||||
/// <param name="parentNodeId">The parent node ID, or null if this is a root folder.</param>
|
||||
/// <param name="displayName">The display name of the folder.</param>
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName)
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> Calls.Enqueue($"EF:{folderNodeId}");
|
||||
/// <summary>Records a variable ensure call.</summary>
|
||||
/// <param name="variableNodeId">The variable node ID.</param>
|
||||
@@ -466,15 +466,15 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
/// <param name="dataType">The OPC UA built-in type name.</param>
|
||||
/// <param name="writable">Whether the node is created read/write.</param>
|
||||
/// <param name="historianTagname">The resolved historian tagname (null ⇒ not historized).</param>
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null)
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> Calls.Enqueue($"EV:{variableNodeId}");
|
||||
/// <summary>Records a rebuild address space call.</summary>
|
||||
public void RebuildAddressSpace() => Interlocked.Increment(ref RebuildCalls);
|
||||
/// <summary>Records a NodeAdded model-change announcement.</summary>
|
||||
/// <param name="affectedNodeId">The node under which discovered nodes were added.</param>
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId) => Calls.Enqueue($"NA:{affectedNodeId}");
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => Calls.Enqueue($"NA:{affectedNodeId}");
|
||||
/// <summary>Records a surgical in-place tag-attribute update (always succeeds in this recording sink).</summary>
|
||||
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength)
|
||||
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
Calls.Enqueue($"UT:{variableNodeId}");
|
||||
return true;
|
||||
@@ -482,25 +482,25 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
/// <summary>Records a surgical in-place folder display-name update (always succeeds in this recording sink).</summary>
|
||||
/// <param name="folderNodeId">The folder node ID to update in place.</param>
|
||||
/// <param name="displayName">The new display name to apply.</param>
|
||||
public bool UpdateFolderDisplayName(string folderNodeId, string displayName)
|
||||
public bool UpdateFolderDisplayName(string folderNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
FolderRenameQueue.Enqueue((folderNodeId, displayName));
|
||||
return true;
|
||||
}
|
||||
/// <summary>Records a surgical variable-node removal (always succeeds in this recording sink).</summary>
|
||||
public bool RemoveVariableNode(string variableNodeId)
|
||||
public bool RemoveVariableNode(string variableNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
Calls.Enqueue($"RV:{variableNodeId}");
|
||||
return true;
|
||||
}
|
||||
/// <summary>Records a surgical alarm-condition-node removal (always succeeds in this recording sink).</summary>
|
||||
public bool RemoveAlarmConditionNode(string alarmNodeId)
|
||||
public bool RemoveAlarmConditionNode(string alarmNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
Calls.Enqueue($"RA:{alarmNodeId}");
|
||||
return true;
|
||||
}
|
||||
/// <summary>Records a surgical equipment-subtree removal (always succeeds in this recording sink).</summary>
|
||||
public bool RemoveEquipmentSubtree(string equipmentNodeId)
|
||||
public bool RemoveEquipmentSubtree(string equipmentNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
Calls.Enqueue($"RE:{equipmentNodeId}");
|
||||
return true;
|
||||
|
||||
@@ -606,14 +606,14 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
/// <param name="value">The attribute value.</param>
|
||||
/// <param name="quality">The OPC UA quality code.</param>
|
||||
/// <param name="ts">The timestamp of the update.</param>
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime ts) =>
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime ts, AddressSpaceRealm realm = AddressSpaceRealm.Uns) =>
|
||||
ValueQueue.Enqueue((nodeId, value, quality, ts));
|
||||
|
||||
/// <summary>Records an alarm condition update.</summary>
|
||||
/// <param name="alarmNodeId">The OPC UA alarm node identifier.</param>
|
||||
/// <param name="state">The full condition state snapshot.</param>
|
||||
/// <param name="ts">The timestamp of the update.</param>
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime ts) =>
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime ts, AddressSpaceRealm realm = AddressSpaceRealm.Uns) =>
|
||||
AlarmQueue.Enqueue((alarmNodeId, state, ts));
|
||||
|
||||
/// <summary>Materialises an alarm condition (no-op in test).</summary>
|
||||
@@ -622,13 +622,13 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
/// <param name="displayName">The condition display name.</param>
|
||||
/// <param name="alarmType">The domain alarm type.</param>
|
||||
/// <param name="severity">The domain severity.</param>
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
|
||||
/// <summary>Records a folder ensure call.</summary>
|
||||
/// <param name="folderNodeId">The OPC UA folder node identifier.</param>
|
||||
/// <param name="parentNodeId">The parent folder node identifier, or null for root.</param>
|
||||
/// <param name="displayName">The display name of the folder.</param>
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) =>
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns) =>
|
||||
FolderQueue.Enqueue((folderNodeId, parentNodeId, displayName));
|
||||
|
||||
/// <summary>Records a variable ensure call.</summary>
|
||||
@@ -638,7 +638,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
/// <param name="dataType">The OPC UA built-in type name.</param>
|
||||
/// <param name="writable">Whether the node is created read/write.</param>
|
||||
/// <param name="historianTagname">The resolved historian tagname (null ⇒ not historized).</param>
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null) =>
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns) =>
|
||||
VariableQueue.Enqueue((variableNodeId, parentFolderNodeId, displayName, dataType, writable));
|
||||
|
||||
/// <summary>Records a rebuild call.</summary>
|
||||
@@ -646,7 +646,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
|
||||
/// <summary>Records a NodeAdded model-change announcement.</summary>
|
||||
/// <param name="affectedNodeId">The node under which discovered nodes were added.</param>
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId) => ModelChangeQueue.Enqueue(affectedNodeId);
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => ModelChangeQueue.Enqueue(affectedNodeId);
|
||||
}
|
||||
|
||||
/// <summary>Test implementation of IServiceLevelPublisher that records publishes.</summary>
|
||||
|
||||
Reference in New Issue
Block a user