namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
///
/// PR abcip-3.3 — sparse-UDT read planner. Where 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.
///
///
/// Pure function — like its sibling planner, this one never touches the runtime + never
/// reads the PLC. It produces the plan; executes it.
///
/// The planner is intentionally libplctag-agnostic: the output is just a list of
/// 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
/// AbCipMultiPacketReadPlannerTests assert plan shape rather than wire bytes.
///
/// Auto-mode dispatch (the heuristic): callers run
/// for each parent UDT to pick between the WholeUdt and MultiPacket paths per-group. The
/// heuristic divides subscribedMembers / totalMembers and picks MultiPacket when the
/// fraction is strictly less than the device's
/// .
///
public static class AbCipMultiPacketReadPlanner
{
///
/// Build a multi-packet read plan from . Members of the same
/// parent UDT collapse into one ; references that
/// don't resolve to a UDT member fall back to for the
/// existing per-tag read path.
///
public static AbCipMultiPacketReadPlan Build(
IReadOnlyList requests,
IReadOnlyDictionary tagsByName)
{
ArgumentNullException.ThrowIfNull(requests);
ArgumentNullException.ThrowIfNull(tagsByName);
var fallback = new List(requests.Count);
var byParent = new Dictionary>(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();
byParent[parentName] = members;
}
members.Add(new AbCipUdtReadMember(i, def, offset));
}
var batches = new List(byParent.Count);
foreach (var (parentName, members) in byParent)
{
batches.Add(new AbCipMultiPacketReadBatch(parentName, tagsByName[parentName], members));
}
return new AbCipMultiPacketReadPlan(batches, fallback);
}
///
/// PR abcip-3.3 — Auto-mode heuristic. For a single parent UDT group with
/// of declared
/// members, pick when sparsity is strictly below
/// , else . Threshold is
/// clamped to [0..1]; out-of-range values saturate. Edge cases:
/// totalMembers == 0 defaults to (the
/// historical behaviour) so a misconfigured tag map doesn't fault the read.
///
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)..]);
}
}
/// A planner output: per-parent multi-packet batches + per-tag fallbacks.
public sealed record AbCipMultiPacketReadPlan(
IReadOnlyList Batches,
IReadOnlyList Fallbacks);
///
/// One UDT parent whose subscribed members are bundled into a Multi-Service Packet read.
/// Reuses from the WholeUdt planner so callers can decode
/// the member offsets uniformly across both planners.
///
public sealed record AbCipMultiPacketReadBatch(
string ParentName,
AbCipTagDefinition ParentDefinition,
IReadOnlyList Members);