feat(opcua): FB-7 surgical DataType/array-shape in-place tag writes
Widen the F10b surgical address-space path so a changed equipment tag whose only differences are DataType / IsArray / ArrayLength (on top of the existing Writable / Historizing) is applied IN PLACE on the live node instead of forcing a full RebuildAddressSpace that drops every client's subscriptions server-wide. ISurgicalAddressSpaceSink.UpdateTagAttributes gains (dataType, isArray, arrayLength); the DeferredAddressSpaceSink wrapper forwards all six args (the prod-inertness seam). OtOpcUaNodeManager swaps DataType + ValueRank + ArrayDimensions in place, and on a real shape change (a) resets the node to BadWaitingForInitialData so no stale wrong-typed value is exposed (closes the prior brief-value-type-mismatch objection) and (b) raises a Part 3 GeneralModelChangeEvent (verb=DataTypeChanged) so model-aware clients re-read the definition. A Writable/Historizing-only change leaves the shape untouched (no reset, no model event) — original behaviour preserved byte-for-byte. AddressSpaceApplier.TagDeltaIsSurgicalEligible adds the three shape fields to its whitelist; FullName/Name/DriverInstanceId/alarm differences still rebuild. Tests: new NodeManagerSurgicalShapeUpdateTests boots a real server to prove the in-place swap + value reset + the no-reset backward-compat path + the model-event builder; AddressSpaceApplierTests invert the two former DataType/IsArray-rebuild cases to surgical and assert the shape args land; DeferredAddressSpaceSinkTests assert the shape args forward. 273/273 OpcUaServer.Tests green; full solution builds.
This commit is contained in:
@@ -74,12 +74,17 @@ public sealed class DeferredAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgical
|
||||
/// 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>
|
||||
/// inert on every driver-role host, because actors inject THIS wrapper, not the inner sink. ALL six args
|
||||
/// (including the FB-7 DataType/array-shape ones) MUST be forwarded — a partial forward silently drops the
|
||||
/// shape update on every driver-role host.</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>
|
||||
/// <param name="dataType">The OPC UA built-in data type name to apply in place.</param>
|
||||
/// <param name="isArray">When true the node becomes a 1-D array; when false scalar.</param>
|
||||
/// <param name="arrayLength">The declared length of the 1-D array when <paramref name="isArray"/> is true.</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)
|
||||
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength)
|
||||
=> _inner is ISurgicalAddressSpaceSink surgical
|
||||
&& surgical.UpdateTagAttributes(variableNodeId, writable, historianTagname);
|
||||
&& surgical.UpdateTagAttributes(variableNodeId, writable, historianTagname, dataType, isArray, arrayLength);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,25 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
|
||||
/// <summary>Optional capability on an address-space sink: surgical in-place attribute updates on an
|
||||
/// EXISTING variable node, used by AddressSpaceApplier to avoid a full RebuildAddressSpace for pure-property
|
||||
/// tag changes (Writable / Historizing). A sink that does not implement it ⇒ caller falls back to a
|
||||
/// full rebuild (safe default).</summary>
|
||||
/// tag changes (Writable / Historizing / DataType / array-shape). A sink that does not implement it ⇒ caller
|
||||
/// falls back to a full rebuild (safe default).</summary>
|
||||
public interface ISurgicalAddressSpaceSink
|
||||
{
|
||||
/// <summary>Update an existing variable node's Writable (AccessLevel + inbound-write handler) and
|
||||
/// Historizing (+ historian-tagname binding) IN PLACE, notifying subscribers (ClearChangeMasks)
|
||||
/// without a rebuild. Returns false if the node does not exist (caller should rebuild instead).</summary>
|
||||
bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname);
|
||||
/// <summary>Update an existing variable node's surgically-updatable attributes IN PLACE, notifying
|
||||
/// subscribers (ClearChangeMasks) without a rebuild — so client MonitoredItems on the node survive.
|
||||
/// Covers Writable (AccessLevel + inbound-write handler), Historizing (+ historian-tagname binding),
|
||||
/// and the node's presentation shape: <paramref name="dataType"/> (the OPC UA DataType) and
|
||||
/// <paramref name="isArray"/>/<paramref name="arrayLength"/> (ValueRank + ArrayDimensions). When the
|
||||
/// shape actually changes, the implementation resets the node's value to BadWaitingForInitialData
|
||||
/// (no stale wrong-typed value is exposed until the driver republishes) and raises a Part 3
|
||||
/// GeneralModelChangeEvent (verb=DataTypeChanged) so model-aware clients re-read the node definition.
|
||||
/// Returns false if the node does not exist (caller should rebuild instead).</summary>
|
||||
/// <param name="variableNodeId">The folder-scoped node id of the variable to update in place.</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>
|
||||
/// <param name="dataType">The OPC UA built-in data type name (e.g. "Boolean", "Int32", "Float").</param>
|
||||
/// <param name="isArray">When true the node is a 1-D array (ValueRank=OneDimension); when false scalar.</param>
|
||||
/// <param name="arrayLength">The declared length of the 1-D array when <paramref name="isArray"/> is true; ignored for scalars.</param>
|
||||
/// <returns>True when the in-place update was applied; false when the node is missing (caller rebuilds).</returns>
|
||||
bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user