feat(opcuaclient): subscription tuning, OperationLimits chunking, diagnostics counters, revocation options, DiscoveryUrl (#273 #275 #276 #277 #278)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;
|
||||
|
||||
/// <summary>
|
||||
/// The remote server's <c>Server/ServerCapabilities/OperationLimits</c> that bound how many
|
||||
/// items one service call may carry (#275). Every field is <c>0</c> = "no limit declared" (the
|
||||
/// OPC UA convention), so <see cref="None"/> is the safe value when the server publishes nothing
|
||||
/// or the read fails.
|
||||
/// </summary>
|
||||
/// <param name="MaxNodesPerRead">Items per Read.</param>
|
||||
/// <param name="MaxNodesPerWrite">Items per Write.</param>
|
||||
/// <param name="MaxNodesPerBrowse">Nodes per Browse.</param>
|
||||
/// <param name="MaxMonitoredItemsPerCall">Items per CreateMonitoredItems / Modify / SetMonitoringMode.</param>
|
||||
/// <param name="MaxNodesPerHistoryReadData">Nodes per HistoryRead (raw / processed / at-time).</param>
|
||||
/// <param name="MaxNodesPerHistoryReadEvents">Nodes per HistoryRead (events).</param>
|
||||
internal sealed record OpcUaClientOperationLimits(
|
||||
uint MaxNodesPerRead,
|
||||
uint MaxNodesPerWrite,
|
||||
uint MaxNodesPerBrowse,
|
||||
uint MaxMonitoredItemsPerCall,
|
||||
uint MaxNodesPerHistoryReadData,
|
||||
uint MaxNodesPerHistoryReadEvents)
|
||||
{
|
||||
/// <summary>No limits declared — every call goes out in one request.</summary>
|
||||
public static OpcUaClientOperationLimits None { get; } = new(0, 0, 0, 0, 0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// Split <paramref name="total"/> items into consecutive <c>(Start, Count)</c> chunks of at most
|
||||
/// <paramref name="limit"/> each. <c>limit == 0</c> (no limit) or a total that already fits yields a
|
||||
/// single chunk; <c>total == 0</c> yields none.
|
||||
/// </summary>
|
||||
/// <param name="total">Number of items to send.</param>
|
||||
/// <param name="limit">The server's per-call cap; 0 = unlimited.</param>
|
||||
/// <returns>The chunk ranges, in order, covering <c>[0, total)</c> exactly once.</returns>
|
||||
public static IEnumerable<(int Start, int Count)> Chunk(int total, uint limit)
|
||||
{
|
||||
if (total <= 0) yield break;
|
||||
if (limit == 0 || limit >= (uint)total)
|
||||
{
|
||||
yield return (0, total);
|
||||
yield break;
|
||||
}
|
||||
var size = (int)Math.Min(limit, int.MaxValue);
|
||||
for (var start = 0; start < total; start += size)
|
||||
yield return (start, Math.Min(size, total - start));
|
||||
}
|
||||
|
||||
/// <summary>The standard variables read to populate the limits, in <see cref="FromValues"/> order.</summary>
|
||||
internal static readonly NodeId[] LimitNodeIds =
|
||||
[
|
||||
VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerRead,
|
||||
VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerWrite,
|
||||
VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerBrowse,
|
||||
VariableIds.Server_ServerCapabilities_OperationLimits_MaxMonitoredItemsPerCall,
|
||||
VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerHistoryReadData,
|
||||
VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerHistoryReadEvents,
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Build limits from the values read for <see cref="LimitNodeIds"/>. A Bad or non-<c>uint</c> value
|
||||
/// (server doesn't publish that limit) is treated as 0 = unlimited.
|
||||
/// </summary>
|
||||
/// <param name="values">One value per <see cref="LimitNodeIds"/> entry, in order.</param>
|
||||
/// <returns>The parsed limits.</returns>
|
||||
public static OpcUaClientOperationLimits FromValues(IReadOnlyList<DataValue> values)
|
||||
{
|
||||
uint At(int i) =>
|
||||
i < values.Count && values[i] is { } dv && StatusCode.IsGood(dv.StatusCode) && dv.Value is uint u ? u : 0;
|
||||
return new OpcUaClientOperationLimits(At(0), At(1), At(2), At(3), At(4), At(5));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the server's declared limits. Best-effort: any failure (older server without the
|
||||
/// capability object, transport error) yields <see cref="None"/> so connect never fails over this.
|
||||
/// </summary>
|
||||
/// <param name="session">The freshly connected session.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The declared limits, or <see cref="None"/>.</returns>
|
||||
public static async Task<OpcUaClientOperationLimits> ReadAsync(ISession session, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var nodes = new ReadValueIdCollection(LimitNodeIds.Length);
|
||||
foreach (var id in LimitNodeIds)
|
||||
nodes.Add(new ReadValueId { NodeId = id, AttributeId = Attributes.Value });
|
||||
var resp = await session.ReadAsync(null, 0, TimestampsToReturn.Neither, nodes, ct).ConfigureAwait(false);
|
||||
return FromValues(resp.Results);
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch { return None; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user