feat(opcua): add ISurgicalAddressSpaceSink + node-manager in-place tag-attribute update (F10b)

This commit is contained in:
Joseph Doherty
2026-06-18 13:32:53 -04:00
parent acc6a64b26
commit 3fc258bd42
3 changed files with 59 additions and 4 deletions
@@ -1332,9 +1332,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
// access bit (on top of the writable composite) so clients can browse + HistoryRead it.
var historized = historianTagname is not null;
// The SDK exposes the flags separately (no CurrentReadWrite composite): ReadWrite is
// CurrentRead | CurrentWrite. OR-ing two byte constants promotes to int, so cast back.
byte access = writable ? (byte)(AccessLevels.CurrentRead | AccessLevels.CurrentWrite) : AccessLevels.CurrentRead;
if (historized) access = (byte)(access | AccessLevels.HistoryRead);
// CurrentRead | CurrentWrite, with the HistoryRead bit OR-ed in when historized.
var access = ComposeAccessLevel(writable, historized);
var variable = new BaseDataVariableState(parent)
{
NodeId = new NodeId(variableNodeId, NamespaceIndex),
@@ -1369,6 +1368,41 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
}
}
/// <summary>Compose the AccessLevel/UserAccessLevel byte for a tag variable from its Writable +
/// Historizing flags — the single source of truth shared by <see cref="EnsureVariable"/> (node
/// creation) and <see cref="UpdateTagAttributes"/> (F10b surgical in-place update). The SDK exposes
/// the flags separately (no CurrentReadWrite composite): ReadWrite is CurrentRead | CurrentWrite,
/// with the HistoryRead bit OR-ed in when historized. OR-ing byte constants promotes to int, so cast back.</summary>
private static byte ComposeAccessLevel(bool writable, bool historized)
{
byte access = writable ? (byte)(AccessLevels.CurrentRead | AccessLevels.CurrentWrite) : AccessLevels.CurrentRead;
if (historized) access = (byte)(access | AccessLevels.HistoryRead);
return access;
}
/// <summary>F10b surgical counterpart of <see cref="EnsureVariable"/>: update an EXISTING tag variable's
/// Writable (AccessLevel + <see cref="OnEquipmentTagWrite"/> handler) and Historizing (+ historian-tagname
/// binding) in place and notify subscribers, WITHOUT a rebuild — so client MonitoredItems on the node
/// survive. Returns false when the node id is unknown (caller falls back to a full rebuild).</summary>
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname)
{
ArgumentException.ThrowIfNullOrEmpty(variableNodeId);
lock (Lock)
{
if (!_variables.TryGetValue(variableNodeId, 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 _);
v.ClearChangeMasks(SystemContext, includeChildren: false);
return true;
}
}
/// <summary>Map a Tag.DataType string ("Boolean", "Int32", "Float", "Double", "String",
/// "DateTime") to the OPC UA built-in NodeId. Unknown names fall back to BaseDataType
/// (matches CreateVariable's default for lazy-created nodes).</summary>
@@ -8,7 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
/// <see cref="OtOpcUaNodeManager"/>. The host wires this in once the StandardServer has
/// been started (so the node manager exists).
/// </summary>
public sealed class SdkAddressSpaceSink : IOpcUaAddressSpaceSink
public sealed class SdkAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgicalAddressSpaceSink
{
private readonly OtOpcUaNodeManager _nodeManager;
@@ -65,6 +65,14 @@ public sealed class SdkAddressSpaceSink : IOpcUaAddressSpaceSink
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);
/// <summary>F10b: surgically update an existing variable node's Writable + Historizing in place
/// (no rebuild). Returns false when the node does not exist (caller falls back to a full rebuild).</summary>
/// <param name="variableNodeId">The variable node identifier.</param>
/// <param name="writable">When true the node becomes read/write with the inbound-write handler; otherwise read-only.</param>
/// <param name="historianTagname">null ⇒ not historized; non-null ⇒ Historizing with the HistoryRead bit and tagname binding.</param>
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname)
=> _nodeManager.UpdateTagAttributes(variableNodeId, writable, historianTagname);
/// <summary>Rebuilds the entire OPC UA address space.</summary>
public void RebuildAddressSpace() => _nodeManager.RebuildAddressSpace();
}