Files
lmxopcua/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/DeferredAddressSpaceSink.cs
T
Joseph Doherty 40e8a23e7c
v2-ci / build (push) Failing after 37s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
refactor(opcuaserver): rename Phase7* address-space pipeline to AddressSpace*
The OPC UA address-space build pipeline was named after a v2-roadmap
milestone number rather than its domain. Rename the family to describe
what it does (build/diff/apply the OPC UA address space):

  Phase7Composer          -> AddressSpaceComposer
  Phase7CompositionResult -> AddressSpaceComposition
  Phase7Planner           -> AddressSpacePlanner
  Phase7Plan              -> AddressSpacePlan
  Phase7Applier           -> AddressSpaceApplier
  Phase7ApplyOutcome      -> AddressSpaceApplyOutcome

The 9 Phase7*Tests suites follow suit; Phase7ScriptingEntitiesTests ->
ScriptingEntitiesTests (it tests the scripting migration, not the
pipeline). Log-message prefixes move to the new class names.

Pure mechanical rename, no behavioral change. EF migration classes/IDs
(AddPhase7ScriptingTables, ExtendComputeGenerationDiffWithPhase7) are
immutable and left untouched, as are historical design docs.

Build clean; OpcUaServer 261/261, Runtime 272/272, ScriptingEntities
12/12 green.
2026-06-18 19:16:28 -04:00

86 lines
6.4 KiB
C#

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