namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// /// Task #194 — groups a ReadAsync batch of full-references into whole-UDT reads where /// possible. A group is emitted for every parent UDT tag whose declared /// s produced a valid offset map AND at least two of /// its members appear in the batch; every other reference stays in the per-tag fallback /// list that runs through its existing read path. /// Pure function — the planner never touches the runtime + never reads the PLC. /// public static class AbCipUdtReadPlanner { /// /// Split into whole-UDT groups + per-tag leftovers. /// is the driver's _tagsByName map — both parent /// UDT rows and their fanned-out member rows live there. Lookup is OrdinalIgnoreCase /// to match the driver's dictionary semantics. /// public static AbCipUdtReadPlan 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)); } // A single-member group saves nothing (one whole-UDT read replaces one per-member read) // — demote to fallback to avoid paying the cost of reading the full UDT buffer only to // pull one field out. var groups = new List(byParent.Count); foreach (var (parentName, members) in byParent) { if (members.Count < 2) { foreach (var m in members) fallback.Add(new AbCipUdtReadFallback(m.OriginalIndex, m.Definition.Name)); continue; } groups.Add(new AbCipUdtReadGroup(parentName, tagsByName[parentName], members)); } return new AbCipUdtReadPlan(groups, fallback); } 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: grouped UDT reads + per-tag fallbacks. public sealed record AbCipUdtReadPlan( IReadOnlyList Groups, IReadOnlyList Fallbacks); /// One UDT parent whose members were batched into a single read. public sealed record AbCipUdtReadGroup( string ParentName, AbCipTagDefinition ParentDefinition, IReadOnlyList Members); /// /// One member inside an . OriginalIndex is the /// slot in the caller's request list so the decoded value lands at the correct output /// offset. Definition is the fanned-out member-level tag definition. Offset /// is the byte offset within the parent UDT buffer where this member lives. /// public sealed record AbCipUdtReadMember(int OriginalIndex, AbCipTagDefinition Definition, int Offset); /// A reference that falls back to the per-tag read path. public sealed record AbCipUdtReadFallback(int OriginalIndex, string Reference);