using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.AbCip.PlcFamilies; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// /// PR abcip-1.4 — multi-tag write planner. Groups a batch of s by /// device so the driver can submit one round of writes per device instead of looping /// strictly serially across the whole batch. Honours the per-family /// flag: families that support /// CIP request packing (ControlLogix / CompactLogix / GuardLogix) issue their writes in /// parallel so libplctag's internal scheduler can coalesce them onto one Multi-Service /// Packet (0x0A); Micro800 (no request packing) falls back to per-tag sequential writes. /// /// /// The libplctag .NET wrapper exposes one CIP service per Tag instance and does /// not surface Multi-Service Packet construction at the API surface — but the underlying /// native library packs concurrent operations against the same connection automatically /// when the family's protocol supports it. Issuing the writes concurrently per device /// therefore gives us the round-trip reduction described in #228 without having to drop to /// raw CIP, while still letting us short-circuit packing on Micro800 where it would be /// unsafe. /// /// Bit-RMW writes (BOOL-with-bitIndex against a DINT parent) are excluded from /// packing here because they need a serialised read-modify-write under the per-parent /// SemaphoreSlim in . Packing two RMWs /// on the same DINT would risk losing one another's update. /// internal static class AbCipMultiWritePlanner { /// /// One classified entry in the input batch. preserves the /// caller's ordering so per-tag StatusCode fan-out lands at the right slot in /// the result array. routes the entry through the RMW path even /// when the device supports packing. /// internal readonly record struct ClassifiedWrite( int OriginalIndex, WriteRequest Request, AbCipTagDefinition Definition, AbCipTagPath? ParsedPath, bool IsBitRmw); /// /// One device's plan slice. entries can be issued concurrently; /// entries must go through the RMW path one-at-a-time per parent /// DINT. /// internal sealed class DevicePlan { public required string DeviceHostAddress { get; init; } public required AbCipPlcFamilyProfile Profile { get; init; } public List Packable { get; } = new(); public List BitRmw { get; } = new(); } /// /// Build the per-device plan list. Entries are visited in input order so the resulting /// plan's traversal preserves caller ordering within each device. Entries that fail /// resolution (unknown reference, non-writable tag, unknown device) are reported via /// with the appropriate StatusCode and excluded from /// the plan. /// public static IReadOnlyList Build( IReadOnlyList writes, IReadOnlyDictionary tagsByName, IReadOnlyDictionary devices, Action reportPreflight) { var plans = new Dictionary(StringComparer.OrdinalIgnoreCase); var order = new List(); for (var i = 0; i < writes.Count; i++) { var w = writes[i]; if (!tagsByName.TryGetValue(w.FullReference, out var def)) { reportPreflight(i, AbCipStatusMapper.BadNodeIdUnknown); continue; } if (!def.Writable || def.SafetyTag) { reportPreflight(i, AbCipStatusMapper.BadNotWritable); continue; } if (!devices.TryGetValue(def.DeviceHostAddress, out var device)) { reportPreflight(i, AbCipStatusMapper.BadNodeIdUnknown); continue; } if (!plans.TryGetValue(def.DeviceHostAddress, out var plan)) { plan = new DevicePlan { DeviceHostAddress = def.DeviceHostAddress, Profile = device.Profile, }; plans[def.DeviceHostAddress] = plan; order.Add(plan); } var parsed = AbCipTagPath.TryParse(def.TagPath); var isBitRmw = def.DataType == AbCipDataType.Bool && parsed?.BitIndex is int; var entry = new ClassifiedWrite(i, w, def, parsed, isBitRmw); if (isBitRmw) plan.BitRmw.Add(entry); else plan.Packable.Add(entry); } return order; } }