namespace ZB.MOM.WW.OtOpcUa.Commons.OpcUa; /// /// Wrapper that defers to an inner sink swapped in at /// runtime. Needed because the production sink (SdkAddressSpaceSink) wraps an /// OtOpcUaNodeManager that only exists after the SDK StandardServer has /// started — but Akka actors resolve their sink dependency at construction time, before /// the hosted service has booted the SDK. /// /// Bound as a singleton in DI on driver-role hosts; the OPC UA hosted service calls /// once the server is up. Until that swap happens, every call is a /// no-op against , so the actor stays safe to /// receive messages from the moment it boots. /// public sealed class DeferredAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgicalAddressSpaceSink { private volatile IOpcUaAddressSpaceSink _inner = NullOpcUaAddressSpaceSink.Instance; /// Swap in the production sink. Pass null to revert to the null sink /// (used during graceful shutdown so post-stop writes don't hit a half-disposed manager). /// The sink implementation to use, or null to use the null sink. public void SetSink(IOpcUaAddressSpaceSink? sink) => _inner = sink ?? NullOpcUaAddressSpaceSink.Instance; /// Writes a value to the OPC UA address space through the inner sink. /// The node ID of the variable. /// 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) => _inner.WriteValue(nodeId, value, quality, sourceTimestampUtc); /// Writes a full alarm-condition state through the inner sink. /// The node ID of the alarm condition. /// The full condition state to project onto the node. /// The source timestamp in UTC. public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc) => _inner.WriteAlarmCondition(alarmNodeId, state, sourceTimestampUtc); /// Materialises a real Part 9 alarm-condition node through the inner sink. /// The alarm node ID (== ScriptedAlarmId). /// The equipment folder node ID the condition parents under. /// The human-readable condition name. /// The domain alarm type. /// The domain severity. /// True for a driver-fed (native) equipment-tag alarm; false (default) for a scripted alarm. public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false) => _inner.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative); /// Ensures a folder exists in the address space through the inner sink. /// 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) => _inner.EnsureFolder(folderNodeId, parentNodeId, displayName); /// Ensures a variable exists in the address space through the inner sink. /// The node ID of the variable. /// The node ID of the parent folder, or null for root. /// The display name of the variable. /// The OPC UA data type of the variable. /// When true the node is created read/write; otherwise read-only. /// null ⇒ not historized; non-null ⇒ create Historizing with the /// HistoryRead access bit and register the historian tagname. /// When true the node is created as a 1-D array; when false (default) scalar. /// The declared length of the 1-D array when is true. 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); /// Rebuilds the address space through the inner sink. public void RebuildAddressSpace() => _inner.RebuildAddressSpace(); /// Forwards an in-place tag-attribute update (F10b) to the inner sink when it supports the /// surgical capability. Returns false otherwise — before the real SdkAddressSpaceSink is /// swapped in (inner is still the null sink), or any inner sink that isn't surgical — so the caller /// (AddressSpaceApplier) falls back to a full rebuild. Without this forward the surgical optimization is /// inert on every driver-role host, because actors inject THIS wrapper, not the inner sink. /// The node ID of the variable to update in place. /// Whether the node should be read/write. /// null ⇒ not historized; non-null ⇒ Historizing + historian binding. /// True when the inner sink applied the update; false when it lacks the capability or the node is missing. public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname) => _inner is ISurgicalAddressSpaceSink surgical && surgical.UpdateTagAttributes(variableNodeId, writable, historianTagname); }