Auto: abcip-3.3 — read-strategy selector (WholeUdt / MultiPacket / Auto)
Closes #237
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user