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);