52708bff52
MAJOR-1: R4's exact element-type match refused ScadaBridge's CLR-list writes (its element types are only Boolean/Int32/Float/Double/String/ DateTime) to every Int16/UInt16/Byte/UInt32/Int64/UInt64 array node and a Double list to a Float array. EvaluateEquipmentWriteStructure now converts a numeric array of another numeric element type element-wise, CHECKED, into the node's element type and hands the converted array back through OnEquipmentTagWrite's ref value, so the node stores and the driver receives the node's own type. Any out-of-range, fractional-to-integer, NaN/Inf-to- integer or Float-overflowing element refuses the whole write BadTypeMismatch (nothing dispatched) - so the UInt32 -> Int32 bit reinterpretation stays closed. Non-numeric mismatches stay refused. The wrong 'no client relies on a looser rule' comment is corrected. MINOR-1: the array-value-to-scalar-node refusal broke writes back to browse-committed array tags (committed scalar, isArray never set) on pass-through drivers. It now applies only when the owning driver's scalar encoder cannot take an array (DriverArrayWriteSupport. RefusesArrayValueOnScalarTag: Modbus, S7), carried RawTagPlan. ScalarRefusesArrayValue / UnsReferenceVariable.BackingScalarRefusesArrayValue -> EnsureVariable(refuseArrayOnScalar) -> a per-node set in the node manager. OpcUaClient/Galaxy/others keep pre-#610 dispatch. Not on the classifier whitelist (driver-type edit rebuilds); a mirror whose flag changes is recreated, not updated in place. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
117 lines
8.2 KiB
C#
117 lines
8.2 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;
|
|
|
|
/// <inheritdoc />
|
|
public void WriteValue(string nodeId, object? value, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm)
|
|
=> _inner.WriteValue(nodeId, value, statusCode, sourceTimestampUtc, realm);
|
|
|
|
/// <inheritdoc />
|
|
// Forward the batch to the inner sink. Without this the interface's default per-item loop runs here and
|
|
// the one-lock-per-batch win is silently degraded back to one lock per value on every driver-role host
|
|
// (actors inject THIS wrapper, not the inner SdkAddressSpaceSink) — same trap as WireAlarmNotifiers above.
|
|
public void WriteValues(IReadOnlyList<ValueWrite> values) => _inner.WriteValues(values);
|
|
|
|
/// <inheritdoc />
|
|
// Forward the status-only batch (03/S15). Unforwarded, the interface's DEFAULT runs here instead —
|
|
// and that default writes a NULL value, so a comms-loss annotation would erase the last known value
|
|
// of every node on the driver rather than retaining it, on every driver-role host (actors inject
|
|
// THIS wrapper, not the inner SdkAddressSpaceSink). Same trap as WriteValues / WireAlarmNotifiers
|
|
// above, except this one degrades to wrong data rather than to slow.
|
|
public void WriteStatuses(IReadOnlyList<StatusWrite> statuses) => _inner.WriteStatuses(statuses);
|
|
|
|
/// <inheritdoc />
|
|
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm)
|
|
=> _inner.WriteAlarmCondition(alarmNodeId, state, sourceTimestampUtc, realm);
|
|
|
|
public void WriteAlarmQuality(string alarmNodeId, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm)
|
|
=> _inner.WriteAlarmQuality(alarmNodeId, statusCode, sourceTimestampUtc, realm);
|
|
|
|
/// <inheritdoc />
|
|
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, AddressSpaceRealm realm, bool isNative = false)
|
|
=> _inner.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, realm, isNative);
|
|
|
|
/// <inheritdoc />
|
|
// Forward the WP4 multi-notifier wiring to the inner sink. Without this the native-alarm fan-out to the
|
|
// referencing equipment folders ships INERT on every driver-role host (actors inject THIS wrapper, not the
|
|
// inner SdkAddressSpaceSink) — the F10b / PR#423 forwarding trap the reflection guard exists to catch.
|
|
public void WireAlarmNotifiers(string alarmNodeId, AddressSpaceRealm alarmRealm, IReadOnlyList<string> notifierFolderNodeIds, AddressSpaceRealm notifierFolderRealm)
|
|
=> _inner.WireAlarmNotifiers(alarmNodeId, alarmRealm, notifierFolderNodeIds, notifierFolderRealm);
|
|
|
|
/// <inheritdoc />
|
|
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm)
|
|
=> _inner.EnsureFolder(folderNodeId, parentNodeId, displayName, realm);
|
|
|
|
/// <inheritdoc />
|
|
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, AddressSpaceRealm realm, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, bool refuseArrayOnScalar = false)
|
|
=> _inner.EnsureVariable(variableNodeId, parentFolderNodeId, displayName, dataType, writable, realm, historianTagname, isArray, arrayLength, refuseArrayOnScalar);
|
|
|
|
/// <inheritdoc />
|
|
public void RebuildAddressSpace() => _inner.RebuildAddressSpace();
|
|
|
|
/// <inheritdoc />
|
|
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm) => _inner.RaiseNodesAddedModelChange(affectedNodeId, realm);
|
|
|
|
/// <inheritdoc />
|
|
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes")
|
|
=> _inner.AddReference(sourceNodeId, sourceRealm, targetNodeId, targetRealm, referenceType);
|
|
|
|
/// <inheritdoc />
|
|
// Forwards 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. 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, AddressSpaceRealm realm)
|
|
=> _inner is ISurgicalAddressSpaceSink surgical
|
|
&& surgical.UpdateTagAttributes(variableNodeId, writable, historianTagname, dataType, isArray, arrayLength, realm);
|
|
|
|
/// <inheritdoc />
|
|
// Forwards 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 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, AddressSpaceRealm realm)
|
|
=> _inner is ISurgicalAddressSpaceSink surgical
|
|
&& 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
|
|
// otherwise (before the real SdkAddressSpaceSink is swapped in, or any non-surgical inner) so the caller
|
|
// (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.
|
|
/// <inheritdoc />
|
|
public bool RemoveVariableNode(string variableNodeId, AddressSpaceRealm realm)
|
|
=> _inner is ISurgicalAddressSpaceSink surgical && surgical.RemoveVariableNode(variableNodeId, realm);
|
|
|
|
/// <inheritdoc />
|
|
public bool RemoveAlarmConditionNode(string alarmNodeId, AddressSpaceRealm realm)
|
|
=> _inner is ISurgicalAddressSpaceSink surgical && surgical.RemoveAlarmConditionNode(alarmNodeId, realm);
|
|
|
|
/// <inheritdoc />
|
|
public bool RemoveEquipmentSubtree(string equipmentNodeId, AddressSpaceRealm realm)
|
|
=> _inner is ISurgicalAddressSpaceSink surgical && surgical.RemoveEquipmentSubtree(equipmentNodeId, realm);
|
|
}
|