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