diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/DeferredAddressSpaceSink.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/DeferredAddressSpaceSink.cs
index f70d8be2..d4e0e39f 100644
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/DeferredAddressSpaceSink.cs
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/DeferredAddressSpaceSink.cs
@@ -23,30 +23,30 @@ public sealed class DeferredAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgical
_inner = sink ?? NullOpcUaAddressSpaceSink.Instance;
///
- 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);
///
- 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);
///
- 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);
///
- 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);
///
- 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);
///
public void RebuildAddressSpace() => _inner.RebuildAddressSpace();
///
- public void RaiseNodesAddedModelChange(string affectedNodeId) => _inner.RaiseNodesAddedModelChange(affectedNodeId);
+ public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => _inner.RaiseNodesAddedModelChange(affectedNodeId, realm);
///
// 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);
///
// 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.
///
- 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);
///
- 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);
///
- 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);
}
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/IOpcUaAddressSpaceSink.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/IOpcUaAddressSpaceSink.cs
index 87c9cac7..cfc6800c 100644
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/IOpcUaAddressSpaceSink.cs
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/IOpcUaAddressSpaceSink.cs
@@ -13,7 +13,12 @@ public interface IOpcUaAddressSpaceSink
/// The value to write.
/// The quality status of the value.
/// The source timestamp in UTC.
- void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc);
+ /// Which of the two v3 namespaces ( device tree or
+ /// equipment tree) the 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.
+ // 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);
/// Write an alarm-condition's full Part 9 state. When a real condition node has been
/// materialised for via ,
@@ -25,7 +30,10 @@ public interface IOpcUaAddressSpaceSink
/// The OPC UA node ID of the alarm (== ScriptedAlarmId for materialised conditions).
/// The full condition state to project onto the node.
/// The source timestamp in UTC.
- void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc);
+ /// The namespace realm lives in (must match the realm
+ /// the condition was materialised under).
+ // 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);
///
/// Materialise a real OPC UA Part 9 alarm-condition node under its equipment folder so clients
@@ -41,7 +49,10 @@ public interface IOpcUaAddressSpaceSink
/// 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.
- void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false);
+ /// The namespace realm the condition (and its parent folder )
+ /// live in.
+ // 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);
///
/// Ensure a folder node exists under the given parent. Used by AddressSpaceApplier to
@@ -52,7 +63,9 @@ public interface IOpcUaAddressSpaceSink
/// The OPC UA node ID for the folder.
/// The parent folder node ID, or null for namespace root.
/// The display name for the folder.
- void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName);
+ /// The namespace realm the folder (and its ) live in.
+ // 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);
///
/// Ensure a Variable node exists at , parented under
@@ -77,7 +90,11 @@ public interface IOpcUaAddressSpaceSink
/// rank + dimensions carry the array-ness.
/// The declared length of the 1-D array when is
/// true; ignored for scalars. Null ⇒ length 0 (unbounded).
- void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null);
+ /// The namespace realm the variable (and its ) live
+ /// in. A historized UNS reference node registers the SAME as its backing
+ /// raw node — both NodeIds map to one tagname — so this call carries the shared tagname per realm.
+ // 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);
///
/// Tear down + repopulate the address space. Called by OpcUaPublishActor after a
@@ -91,7 +108,9 @@ public interface IOpcUaAddressSpaceSink
/// (Part 3 GeneralModelChangeEvent, verb NodeAdded).
///
/// The node under which discovered nodes were added.
- void RaiseNodesAddedModelChange(string affectedNodeId);
+ /// The namespace realm lives in.
+ // transitional default — B4-WP3/Wave B makes every call site explicit and removes this default.
+ void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns);
}
/// OPC UA status code projection — Good / Uncertain / Bad. Real SDK has finer-grained
@@ -106,23 +125,23 @@ public sealed class NullOpcUaAddressSpaceSink : IOpcUaAddressSpaceSink
private NullOpcUaAddressSpaceSink() { }
///
- 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) { }
///
- 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) { }
///
- public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) { }
+ 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) { }
+ 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) { }
}
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/ISurgicalAddressSpaceSink.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/ISurgicalAddressSpaceSink.cs
index f4033030..656fcb78 100644
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/ISurgicalAddressSpaceSink.cs
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/ISurgicalAddressSpaceSink.cs
@@ -21,8 +21,11 @@ public interface ISurgicalAddressSpaceSink
/// The OPC UA built-in data type name (e.g. "Boolean", "Int32", "Float").
/// When true the node is a 1-D array (ValueRank=OneDimension); when false scalar.
/// The declared length of the 1-D array when is true; ignored for scalars.
+ /// The namespace realm lives in — the sink resolves the
+ /// namespace index from the realm rather than parsing it out of the id string.
/// True when the in-place update was applied; false when the node is missing (caller rebuilds).
- 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);
/// Update an existing folder node's DisplayName 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).
/// The node id of the folder whose display name to update in place.
/// The new display name to apply.
+ /// The namespace realm lives in.
/// True when the in-place update was applied; false when the folder is missing (caller rebuilds).
- 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);
/// 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).
/// The folder-scoped node id of the variable to remove in place.
+ /// The namespace realm lives in.
/// True when the node was removed; false when the id is unknown (caller rebuilds).
- 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);
/// 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).
/// The alarm condition node id (== ScriptedAlarmId, or a native alarm tag's folder-scoped id).
+ /// The namespace realm lives in.
/// True when the condition was removed; false when the id is unknown (caller rebuilds).
- 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);
/// 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).
/// The equipment folder node id whose entire subtree to remove.
+ /// The namespace realm lives in.
/// True when the subtree was removed; false when the id is unknown (caller rebuilds).
- 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);
}
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs
index 335840d8..b6375a5e 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs
@@ -36,7 +36,20 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
///
public sealed class OtOpcUaNodeManager : CustomNodeManager2
{
- public const string DefaultNamespaceUri = "https://zb.com/otopcua/ns";
+ /// 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. is
+ /// registered FIRST (namespace index [0]) and
+ /// SECOND ([1]); maps a realm
+ /// to its index. Which realm a node belongs to travels with each sink call as an
+ /// — the namespace is never parsed back out of the id string.
+ private static readonly string[] RegisteredNamespaceUris = { V3NodeIds.RawNamespaceUri, V3NodeIds.UnsNamespaceUri };
+
+ /// Transitional alias kept so the pre-v3 single-namespace integration tests
+ /// (SubscriptionSurvivalTests) still resolve a namespace index. It points at
+ /// — 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.
+ public const string DefaultNamespaceUri = V3NodeIds.UnsNamespaceUri;
private readonly ConcurrentDictionary _variables = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary _folders = new(StringComparer.Ordinal);
@@ -73,11 +86,47 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
/// The OPC UA server instance.
/// The application configuration.
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.
}
+ /// Resolve a realm to the SDK namespace index its nodes are minted under. Raw is registered
+ /// first ([0]), UNS second ([1]) — see .
+ /// The address-space realm.
+ /// The namespace index for the realm.
+ 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."),
+ };
+
+ /// Recover the realm of an already-resolved SDK 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).
+ /// The resolved SDK node id.
+ /// The realm the node belongs to.
+ private AddressSpaceRealm RealmOf(NodeId nodeId) =>
+ nodeId.NamespaceIndex == NamespaceIndexes[0] ? AddressSpaceRealm.Raw : AddressSpaceRealm.Uns;
+
+ /// Build the internal map key for a realm-qualified string id. The key is the node's full
+ /// namespace-qualified NodeId string (e.g. ns=2;s=<id>) so a Raw node and a UNS node that
+ /// share the same s= identifier are distinct keys — a bare id is no longer globally unique across
+ /// the two namespaces. Matches for the same node.
+ /// The realm the id lives in.
+ /// The bare s= identifier.
+ /// The namespace-qualified map key.
+ private string MapKey(AddressSpaceRealm realm, string id) => new NodeId(id, NamespaceIndexForRealm(realm)).ToString();
+
+ /// Build the internal map key for an already-resolved SDK (the SDK-facing
+ /// seams). Equal to for the same node.
+ /// The resolved SDK node id.
+ /// The namespace-qualified map key.
+ private static string MapKey(NodeId nodeId) => nodeId.ToString();
+
/// Gets the count of variable nodes currently managed.
public int VariableCount => _variables.Count;
/// Gets the count of folder nodes currently managed.
@@ -246,8 +295,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
/// ScriptedAlarmId), or null if not yet materialised. Exposed for tests + diagnostics.
/// The alarm node identifier (== ScriptedAlarmId).
/// The cached , or null when none is registered.
- 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;
/// 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
/// The variable node identifier.
/// The resolved historian tagname when historized; otherwise null.
/// True when the node is registered as historized; otherwise false.
- 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;
+ }
+
+ /// 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.
+ /// The resolved SDK node id from the HistoryRead request.
+ /// The resolved historian tagname when historized; otherwise null.
+ /// True when the node is registered as historized; otherwise false.
+ 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.
/// The variable node identifier.
/// The cached , or null when none is registered.
- 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;
/// 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).
/// The folder node identifier.
/// The cached , or null when none is registered.
- 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;
///
/// Apply a value write from . Creates the
@@ -286,19 +349,21 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
/// The new value to write.
/// The OPC UA quality status code.
/// The timestamp of the value in UTC.
- 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
/// The node identifier of the alarm (== ScriptedAlarmId for materialised conditions).
/// The full condition state to project onto the node.
/// The timestamp of the alarm state change in UTC.
- 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
/// . LimitAlarm deliberately falls back to base per the T13
/// notes — a script alarm carries no High/Low limits to populate.
///
- 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.
/// The alarm condition node id.
/// True when the condition is native (driver-fed); false when it is scripted or not found.
- 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));
}
///
@@ -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 to populate the audit event's ClientUserId; null when unknown.
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
/// The node identifier of the folder.
/// The node identifier of the parent folder; null to use the namespace root.
/// The display name of the folder.
- 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
/// The node identifier of the folder to update in place.
/// The new display name to apply.
/// True when the in-place update was applied; false when the folder id is unknown.
- 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
/// — rank + dimensions carry the array-ness.
/// Phase 4c: the declared length of the 1-D array when
/// is true; ignored for scalars. Null ⇒ length 0.
- 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
/// When true the node becomes a 1-D array (ValueRank=OneDimension); when false scalar.
/// The declared length of the 1-D array when is true; ignored for scalars.
/// True when the in-place update was applied; false when the node id is unknown.
- 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
///
///
/// The folder-scoped node id of the parent under which nodes were added.
- 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).
/// The folder-scoped node id of the parent under which nodes were added.
/// A populated, unreported .
- 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.
/// The node id of the deleted node.
/// A populated, unreported .
- 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
/// Resolve the TypeDefinition of a materialised node id from the live folder/variable maps for a
/// model-change event's AffectedType; when the id is not registered.
/// The folder-scoped node id whose TypeDefinition is wanted.
- 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
}
///
- 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;
}
///
- 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;
}
///
- 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
- // "/" (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="). Every
+ // descendant in the SAME realm shares that key exactly or begins with "/" (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!;
}
/// Guard the address-space mutators against a too-early call. _root
@@ -2124,8 +2216,9 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
var work = new List>(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> 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,
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs
index 16350b02..65305767 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs
@@ -21,48 +21,48 @@ public sealed class SdkAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgicalAddre
}
///
- 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);
///
- 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);
///
- 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);
///
- 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);
///
- 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);
///
- 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);
///
- 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);
///
- public bool RemoveVariableNode(string variableNodeId)
- => _nodeManager.RemoveVariableNode(variableNodeId);
+ public bool RemoveVariableNode(string variableNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
+ => _nodeManager.RemoveVariableNode(variableNodeId, realm);
///
- public bool RemoveAlarmConditionNode(string alarmNodeId)
- => _nodeManager.RemoveAlarmConditionNode(alarmNodeId);
+ public bool RemoveAlarmConditionNode(string alarmNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
+ => _nodeManager.RemoveAlarmConditionNode(alarmNodeId, realm);
///
- public bool RemoveEquipmentSubtree(string equipmentNodeId)
- => _nodeManager.RemoveEquipmentSubtree(equipmentNodeId);
+ public bool RemoveEquipmentSubtree(string equipmentNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
+ => _nodeManager.RemoveEquipmentSubtree(equipmentNodeId, realm);
///
public void RebuildAddressSpace() => _nodeManager.RebuildAddressSpace();
///
- public void RaiseNodesAddedModelChange(string affectedNodeId) => _nodeManager.RaiseNodesAddedModelChange(affectedNodeId);
+ public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => _nodeManager.RaiseNodesAddedModelChange(affectedNodeId, realm);
}
diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/DeferredAddressSpaceSinkTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/DeferredAddressSpaceSinkTests.cs
index 6b2e2ae8..33082333 100644
--- a/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/DeferredAddressSpaceSinkTests.cs
+++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/DeferredAddressSpaceSinkTests.cs
@@ -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; }
}
}
diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/DeferredSinkForwardingReflectionTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/DeferredSinkForwardingReflectionTests.cs
index 7903ac34..5a005ee0 100644
--- a/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/DeferredSinkForwardingReflectionTests.cs
+++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/DeferredSinkForwardingReflectionTests.cs
@@ -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()
{
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierFailureSurfaceTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierFailureSurfaceTests.cs
index 9582c8d5..6e6d0729 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierFailureSurfaceTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierFailureSurfaceTests.cs
@@ -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) { }
}
}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierHierarchyTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierHierarchyTests.cs
index 14cc8f87..c5210663 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierHierarchyTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierHierarchyTests.cs
@@ -307,24 +307,24 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable
/// The value to write.
/// The OPC UA quality value.
/// The source timestamp in UTC.
- 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) { }
/// Records an alarm condition write (stub implementation for testing).
/// The node ID of the alarm condition.
/// The full condition state snapshot.
/// The source timestamp in UTC.
- public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc) { }
+ public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
/// Materialises an alarm condition (stub implementation for testing).
/// The alarm node ID (== ScriptedAlarmId).
/// The equipment folder node ID.
/// The condition display name.
/// The domain alarm type.
/// The domain severity.
- 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) { }
/// Records a folder creation request.
/// The node ID of the folder.
/// The node ID of the parent folder, or null for root.
/// The display name of the folder.
- 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));
/// Ensures a variable exists (stub implementation for testing).
/// The node ID of the variable.
@@ -333,10 +333,10 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable
/// The OPC UA built-in type name.
/// Whether the node is created read/write.
/// The resolved historian tagname (null ⇒ not historized).
- 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) { }
/// Rebuilds the address space (stub implementation for testing).
public void RebuildAddressSpace() { }
/// Announces a NodeAdded model-change (stub implementation for testing).
- public void RaiseNodesAddedModelChange(string affectedNodeId) { }
+ public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
}
}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierTests.cs
index 97dfefc5..5d6ff679 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierTests.cs
@@ -2170,7 +2170,7 @@ public sealed class AddressSpaceApplierTests
/// The new OPC UA data type name to apply in place.
/// The new array-ness of the node.
/// The new 1-D array length when is true.
- 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
/// Records a surgical in-place folder display-name update; returns .
/// The folder node ID to update in place.
/// The new display name to apply.
- 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;
/// Records a surgical variable-node removal; returns .
- public bool RemoveVariableNode(string variableNodeId)
+ public bool RemoveVariableNode(string variableNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
{
RemoveQueue.Enqueue(("var", variableNodeId));
return RemoveReturns;
}
/// Records a surgical alarm-condition-node removal; returns .
- public bool RemoveAlarmConditionNode(string alarmNodeId)
+ public bool RemoveAlarmConditionNode(string alarmNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
{
RemoveQueue.Enqueue(("alarm", alarmNodeId));
return RemoveReturns;
}
/// Records a surgical equipment-subtree removal; returns .
- 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
/// The value to write.
/// The OPC UA quality.
/// The source timestamp in UTC.
- 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));
/// Records an alarm condition write call.
/// The alarm node ID.
/// The full condition state snapshot.
/// The source timestamp in UTC.
- 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));
/// Records an alarm-condition materialise call.
/// The alarm node ID (== ScriptedAlarmId).
@@ -2276,13 +2276,13 @@ public sealed class AddressSpaceApplierTests
/// The condition display name.
/// The domain alarm type.
/// The domain severity.
- 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));
/// Records a folder creation call.
/// The folder node ID.
/// The parent folder node ID, if any.
/// The display name for the folder.
- 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));
/// Records a variable creation call.
/// The variable node ID.
@@ -2291,7 +2291,7 @@ public sealed class AddressSpaceApplierTests
/// The OPC UA built-in type name.
/// Whether the node is created read/write.
/// The resolved historian tagname (null ⇒ not historized).
- 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 ModelChangeCalls => ModelChangeQueue.ToList();
/// Records a NodeAdded model-change announcement.
/// The node under which discovered nodes were added.
- public void RaiseNodesAddedModelChange(string affectedNodeId) => ModelChangeQueue.Enqueue(affectedNodeId);
+ public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => ModelChangeQueue.Enqueue(affectedNodeId);
}
/// A recording sink that does NOT implement — used to
@@ -2318,19 +2318,19 @@ public sealed class AddressSpaceApplierTests
public int RebuildCalls;
/// Records a value write (no-op in this sink).
- 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) { }
/// No-op alarm condition write call.
- public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc) { }
+ public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
/// No-op alarm-condition materialise call.
- 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) { }
/// No-op folder creation call.
- public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) { }
+ public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
/// No-op variable creation call.
- 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) { }
/// Records a rebuild address space call.
public void RebuildAddressSpace() => Interlocked.Increment(ref RebuildCalls);
/// No-op NodeAdded model-change announcement.
- 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
/// The value to write.
/// The OPC UA quality.
/// The source timestamp in UTC.
- 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) { }
/// Throws an exception if configured to do so.
/// The alarm node ID.
/// The full condition state snapshot.
/// The source timestamp in UTC.
/// Thrown when configured to throw on alarm write.
- 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
/// The condition display name.
/// The domain alarm type.
/// The domain severity.
- 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) { }
/// No-op folder creation call.
/// The folder node ID.
/// The parent folder node ID, if any.
/// The display name for the folder.
- public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) { }
+ public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
/// No-op variable creation call.
/// The variable node ID.
/// The parent folder node ID, if any.
@@ -2374,10 +2374,10 @@ public sealed class AddressSpaceApplierTests
/// The OPC UA built-in type name.
/// Whether the node is created read/write.
/// The resolved historian tagname (null ⇒ not historized).
- 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) { }
/// No-op rebuild address space call.
public void RebuildAddressSpace() { }
/// No-op NodeAdded model-change announcement.
- public void RaiseNodesAddedModelChange(string affectedNodeId) { }
+ public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
}
}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/DeferredAddressSpaceSinkTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/DeferredAddressSpaceSinkTests.cs
index d10a9f58..2265421e 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/DeferredAddressSpaceSinkTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/DeferredAddressSpaceSinkTests.cs
@@ -228,19 +228,19 @@ public sealed class DeferredAddressSpaceSinkTests
public List<(string NodeId, string? HistorianTagname)> HistorianCalls => HistorianQueue.ToList();
///
- 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}");
///
- 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}");
///
- 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}");
///
- 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}");
///
- 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
///
public void RebuildAddressSpace() => CallQueue.Enqueue("RB");
///
- 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();
///
- 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;
}
///
- 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();
///
- 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; }
///
- 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; }
///
- 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; }
///
- 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) { }
///
- 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) { }
///
- public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) { }
+ 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) { }
+ 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) { }
}
}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/SdkAddressSpaceSinkTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/SdkAddressSpaceSinkTests.cs
index 85301fc4..495c8a82 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/SdkAddressSpaceSinkTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/SdkAddressSpaceSinkTests.cs
@@ -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();
- 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();
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DiscoveryInjectionEndToEndTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DiscoveryInjectionEndToEndTests.cs
index 94088347..aeee15cc 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DiscoveryInjectionEndToEndTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DiscoveryInjectionEndToEndTests.cs
@@ -324,27 +324,27 @@ public sealed class DiscoveryInjectionEndToEndTests : RuntimeActorTestBase
public int RebuildCalls;
/// Records a live-value write.
- 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));
/// No-op: alarm writes are not exercised by this suite.
- public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc) { }
+ public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
/// No-op: alarm materialise is not exercised by this suite.
- 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) { }
/// Records an EnsureFolder call.
- 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));
/// Records an EnsureVariable call.
- 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));
/// Records a raw rebuild (the apply-time fallback when no dbFactory is wired).
public void RebuildAddressSpace() => Interlocked.Increment(ref RebuildCalls);
/// Records a NodeAdded model-change announcement.
- public void RaiseNodesAddedModelChange(string affectedNodeId) => _modelChanges.Enqueue(affectedNodeId);
+ public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => _modelChanges.Enqueue(affectedNodeId);
}
}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Observability/OtOpcUaTelemetryHookTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Observability/OtOpcUaTelemetryHookTests.cs
index 78063069..751d2c11 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Observability/OtOpcUaTelemetryHookTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Observability/OtOpcUaTelemetryHookTests.cs
@@ -194,24 +194,24 @@ public sealed class OtOpcUaTelemetryHookTests : RuntimeActorTestBase
/// The value being written.
/// The OPC UA quality status.
/// The source timestamp in UTC.
- 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++;
/// Records an alarm condition write.
/// The alarm node identifier.
/// The full condition state snapshot.
/// The time the alarm occurred in UTC.
- public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime occurredUtc) => Writes++;
+ public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime occurredUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => Writes++;
/// Materialises an alarm condition (stub implementation).
/// The alarm node identifier.
/// The equipment folder node identifier.
/// The condition display name.
/// The domain alarm type.
/// The domain severity.
- 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) { }
/// Ensures folder exists (stub implementation).
/// The folder node identifier.
/// The parent folder node identifier.
/// The display name for the folder.
- public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) { }
+ public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
/// Ensures variable exists (stub implementation).
/// The variable node identifier.
/// The parent folder node identifier.
@@ -219,10 +219,10 @@ public sealed class OtOpcUaTelemetryHookTests : RuntimeActorTestBase
/// The OPC UA built-in type name.
/// Whether the node is created read/write.
/// The resolved historian tagname (null ⇒ not historized).
- 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) { }
/// Rebuilds address space (recorded via span).
public void RebuildAddressSpace() { /* recorded via span */ }
/// Announces a NodeAdded model-change (stub implementation).
- public void RaiseNodesAddedModelChange(string affectedNodeId) { }
+ public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
}
}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorApplyFailureTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorApplyFailureTests.cs
index 3bb225fa..1e4256ff 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorApplyFailureTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorApplyFailureTests.cs
@@ -118,25 +118,25 @@ public sealed class OpcUaPublishActorApplyFailureTests : RuntimeActorTestBase
/// A sink whose RebuildAddressSpace throws (drives the applier's SafeRebuild catch → RebuildFailed).
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) { }
}
/// A no-op sink — a clean apply (no failures).
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) { }
}
/// Listens to a single instrument by name on the central meter and tallies value + tags.
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorRebuildTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorRebuildTests.cs
index 04894b13..1a1c43d1 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorRebuildTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorRebuildTests.cs
@@ -437,13 +437,13 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
/// The value to write.
/// The OPC UA quality code.
/// The timestamp of the write.
- 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}");
/// Records an alarm condition write call.
/// The alarm node ID.
/// The full condition state snapshot.
/// The timestamp of the state change.
- 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}");
/// Records a materialise-alarm-condition call.
/// The alarm node ID (== ScriptedAlarmId).
@@ -451,13 +451,13 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
/// The condition display name.
/// The domain alarm type.
/// The domain severity.
- 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}");
/// Records a folder ensure call.
/// The folder node ID.
/// The parent node ID, or null if this is a root folder.
/// The display name of the folder.
- 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}");
/// Records a variable ensure call.
/// The variable node ID.
@@ -466,15 +466,15 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
/// The OPC UA built-in type name.
/// Whether the node is created read/write.
/// The resolved historian tagname (null ⇒ not historized).
- 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}");
/// Records a rebuild address space call.
public void RebuildAddressSpace() => Interlocked.Increment(ref RebuildCalls);
/// Records a NodeAdded model-change announcement.
/// The node under which discovered nodes were added.
- public void RaiseNodesAddedModelChange(string affectedNodeId) => Calls.Enqueue($"NA:{affectedNodeId}");
+ public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => Calls.Enqueue($"NA:{affectedNodeId}");
/// Records a surgical in-place tag-attribute update (always succeeds in this recording sink).
- 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
/// Records a surgical in-place folder display-name update (always succeeds in this recording sink).
/// The folder node ID to update in place.
/// The new display name to apply.
- public bool UpdateFolderDisplayName(string folderNodeId, string displayName)
+ public bool UpdateFolderDisplayName(string folderNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
{
FolderRenameQueue.Enqueue((folderNodeId, displayName));
return true;
}
/// Records a surgical variable-node removal (always succeeds in this recording sink).
- public bool RemoveVariableNode(string variableNodeId)
+ public bool RemoveVariableNode(string variableNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
{
Calls.Enqueue($"RV:{variableNodeId}");
return true;
}
/// Records a surgical alarm-condition-node removal (always succeeds in this recording sink).
- public bool RemoveAlarmConditionNode(string alarmNodeId)
+ public bool RemoveAlarmConditionNode(string alarmNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
{
Calls.Enqueue($"RA:{alarmNodeId}");
return true;
}
/// Records a surgical equipment-subtree removal (always succeeds in this recording sink).
- public bool RemoveEquipmentSubtree(string equipmentNodeId)
+ public bool RemoveEquipmentSubtree(string equipmentNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
{
Calls.Enqueue($"RE:{equipmentNodeId}");
return true;
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorTests.cs
index e903b714..7086d3c0 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorTests.cs
@@ -606,14 +606,14 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
/// The attribute value.
/// The OPC UA quality code.
/// The timestamp of the update.
- 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));
/// Records an alarm condition update.
/// The OPC UA alarm node identifier.
/// The full condition state snapshot.
/// The timestamp of the update.
- 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));
/// Materialises an alarm condition (no-op in test).
@@ -622,13 +622,13 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
/// The condition display name.
/// The domain alarm type.
/// The domain severity.
- 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) { }
/// Records a folder ensure call.
/// The OPC UA folder node identifier.
/// The parent folder node identifier, or null for root.
/// The display name of the folder.
- 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));
/// Records a variable ensure call.
@@ -638,7 +638,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
/// The OPC UA built-in type name.
/// Whether the node is created read/write.
/// The resolved historian tagname (null ⇒ not historized).
- 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));
/// Records a rebuild call.
@@ -646,7 +646,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
/// Records a NodeAdded model-change announcement.
/// The node under which discovered nodes were added.
- public void RaiseNodesAddedModelChange(string affectedNodeId) => ModelChangeQueue.Enqueue(affectedNodeId);
+ public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => ModelChangeQueue.Enqueue(affectedNodeId);
}
/// Test implementation of IServiceLevelPublisher that records publishes.