133 lines
6.3 KiB
C#
133 lines
6.3 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
|
|
|
/// <summary>
|
|
/// PR abcip-3.3 — sparse-UDT read planner. Where <see cref="AbCipUdtReadPlanner"/> reads each
|
|
/// parent UDT once and decodes every subscribed member from the buffer in-memory, this planner
|
|
/// keeps the per-member read shape and bundles the reads into one CIP Multi-Service Packet
|
|
/// per parent so a 5-of-50-member subscription doesn't pay for the whole UDT buffer.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>Pure function — like its sibling planner, this one never touches the runtime + never
|
|
/// reads the PLC. It produces the plan; <see cref="AbCipDriver"/> executes it.</para>
|
|
///
|
|
/// <para>The planner is intentionally <c>libplctag</c>-agnostic: the output is just a list of
|
|
/// <see cref="AbCipMultiPacketReadBatch"/> records that name the parent UDT, the per-member
|
|
/// read targets, and their byte offsets. The runtime layer decides whether to issue one
|
|
/// libplctag read per member (today's wrapper-limited fallback) or to flush the batch onto
|
|
/// one Multi-Service Packet (a future wrapper release). Either way the planner-tier logic
|
|
/// stays correct, which is why the unit tests in
|
|
/// <c>AbCipMultiPacketReadPlannerTests</c> assert plan shape rather than wire bytes.</para>
|
|
///
|
|
/// <para>Auto-mode dispatch (the heuristic): callers run <see cref="ChooseStrategyForGroup"/>
|
|
/// for each parent UDT to pick between the WholeUdt and MultiPacket paths per-group. The
|
|
/// heuristic divides <c>subscribedMembers / totalMembers</c> and picks MultiPacket when the
|
|
/// fraction is strictly less than the device's
|
|
/// <see cref="AbCipDeviceOptions.MultiPacketSparsityThreshold"/>.</para>
|
|
/// </remarks>
|
|
public static class AbCipMultiPacketReadPlanner
|
|
{
|
|
/// <summary>
|
|
/// Build a multi-packet read plan from <paramref name="requests"/>. Members of the same
|
|
/// parent UDT collapse into one <see cref="AbCipMultiPacketReadBatch"/>; references that
|
|
/// don't resolve to a UDT member fall back to <see cref="AbCipUdtReadFallback"/> for the
|
|
/// existing per-tag read path.
|
|
/// </summary>
|
|
public static AbCipMultiPacketReadPlan Build(
|
|
IReadOnlyList<string> requests,
|
|
IReadOnlyDictionary<string, AbCipTagDefinition> tagsByName)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(requests);
|
|
ArgumentNullException.ThrowIfNull(tagsByName);
|
|
|
|
var fallback = new List<AbCipUdtReadFallback>(requests.Count);
|
|
var byParent = new Dictionary<string, List<AbCipUdtReadMember>>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
for (var i = 0; i < requests.Count; i++)
|
|
{
|
|
var name = requests[i];
|
|
if (!tagsByName.TryGetValue(name, out var def))
|
|
{
|
|
fallback.Add(new AbCipUdtReadFallback(i, name));
|
|
continue;
|
|
}
|
|
|
|
var (parentName, memberName) = SplitParentMember(name);
|
|
if (parentName is null || memberName is null
|
|
|| !tagsByName.TryGetValue(parentName, out var parent)
|
|
|| parent.DataType != AbCipDataType.Structure
|
|
|| parent.Members is not { Count: > 0 })
|
|
{
|
|
fallback.Add(new AbCipUdtReadFallback(i, name));
|
|
continue;
|
|
}
|
|
|
|
var offsets = AbCipUdtMemberLayout.TryBuild(parent.Members);
|
|
if (offsets is null || !offsets.TryGetValue(memberName, out var offset))
|
|
{
|
|
fallback.Add(new AbCipUdtReadFallback(i, name));
|
|
continue;
|
|
}
|
|
|
|
if (!byParent.TryGetValue(parentName, out var members))
|
|
{
|
|
members = new List<AbCipUdtReadMember>();
|
|
byParent[parentName] = members;
|
|
}
|
|
members.Add(new AbCipUdtReadMember(i, def, offset));
|
|
}
|
|
|
|
var batches = new List<AbCipMultiPacketReadBatch>(byParent.Count);
|
|
foreach (var (parentName, members) in byParent)
|
|
{
|
|
batches.Add(new AbCipMultiPacketReadBatch(parentName, tagsByName[parentName], members));
|
|
}
|
|
|
|
return new AbCipMultiPacketReadPlan(batches, fallback);
|
|
}
|
|
|
|
/// <summary>
|
|
/// PR abcip-3.3 — Auto-mode heuristic. For a single parent UDT group with
|
|
/// <paramref name="subscribedMembers"/> of <paramref name="totalMembers"/> declared
|
|
/// members, pick <see cref="ReadStrategy.MultiPacket"/> when sparsity is strictly below
|
|
/// <paramref name="threshold"/>, else <see cref="ReadStrategy.WholeUdt"/>. Threshold is
|
|
/// clamped to <c>[0..1]</c>; out-of-range values saturate. Edge cases:
|
|
/// <c>totalMembers == 0</c> defaults to <see cref="ReadStrategy.WholeUdt"/> (the
|
|
/// historical behaviour) so a misconfigured tag map doesn't fault the read.
|
|
/// </summary>
|
|
public static ReadStrategy ChooseStrategyForGroup(int subscribedMembers, int totalMembers, double threshold)
|
|
{
|
|
if (totalMembers <= 0) return ReadStrategy.WholeUdt;
|
|
|
|
// Saturate the threshold to a sane range. 0.0 → never MultiPacket; 1.0 → always
|
|
// MultiPacket whenever any member is subscribed (deterministic boundary behaviour).
|
|
var t = threshold;
|
|
if (t < 0.0) t = 0.0;
|
|
if (t > 1.0) t = 1.0;
|
|
|
|
var fraction = (double)subscribedMembers / totalMembers;
|
|
return fraction < t ? ReadStrategy.MultiPacket : ReadStrategy.WholeUdt;
|
|
}
|
|
|
|
private static (string? Parent, string? Member) SplitParentMember(string reference)
|
|
{
|
|
var dot = reference.IndexOf('.');
|
|
if (dot <= 0 || dot == reference.Length - 1) return (null, null);
|
|
return (reference[..dot], reference[(dot + 1)..]);
|
|
}
|
|
}
|
|
|
|
/// <summary>A planner output: per-parent multi-packet batches + per-tag fallbacks.</summary>
|
|
public sealed record AbCipMultiPacketReadPlan(
|
|
IReadOnlyList<AbCipMultiPacketReadBatch> Batches,
|
|
IReadOnlyList<AbCipUdtReadFallback> Fallbacks);
|
|
|
|
/// <summary>
|
|
/// One UDT parent whose subscribed members are bundled into a Multi-Service Packet read.
|
|
/// Reuses <see cref="AbCipUdtReadMember"/> from the WholeUdt planner so callers can decode
|
|
/// the member offsets uniformly across both planners.
|
|
/// </summary>
|
|
public sealed record AbCipMultiPacketReadBatch(
|
|
string ParentName,
|
|
AbCipTagDefinition ParentDefinition,
|
|
IReadOnlyList<AbCipUdtReadMember> Members);
|