Auto: abcip-1.4 — CIP multi-tag write packing
Group writes by device through new AbCipMultiWritePlanner; for families that support CIP request packing (ControlLogix / CompactLogix / GuardLogix) the packable writes for one device are dispatched concurrently so libplctag's native scheduler can coalesce them onto one Multi-Service Packet (0x0A). Micro800 keeps SupportsRequestPacking=false and falls back to per-tag sequential writes. BOOL-within-DINT writes are excluded from packing and continue to go through the per-parent RMW semaphore so two concurrent bit writes against the same DINT cannot lose one another's update. The libplctag .NET wrapper does not expose a Multi-Service Packet construction API at the per-Tag surface (each Tag is one CIP service), so this PR uses client-side coalescing — concurrent Task.WhenAll dispatch per device — rather than building raw CIP frames. The native libplctag scheduler does pack concurrent same-connection writes when the family allows it, which gives the round-trip reduction #228 calls for without ballooning the diff. Per-tag StatusCodes preserve caller order across success, transport failure, non-writable tags, unknown references, and unknown devices, including in mixed concurrent batches. Closes #228
This commit is contained in:
112
src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipMultiWritePlanner.cs
Normal file
112
src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipMultiWritePlanner.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbCip.PlcFamilies;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
|
||||
/// <summary>
|
||||
/// PR abcip-1.4 — multi-tag write planner. Groups a batch of <see cref="WriteRequest"/>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
|
||||
/// <see cref="AbCipPlcFamilyProfile.SupportsRequestPacking"/> 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>The libplctag .NET wrapper exposes one CIP service per <c>Tag</c> 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.</para>
|
||||
///
|
||||
/// <para>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
|
||||
/// <c>SemaphoreSlim</c> in <see cref="AbCipDriver.WriteBitInDIntAsync"/>. Packing two RMWs
|
||||
/// on the same DINT would risk losing one another's update.</para>
|
||||
/// </remarks>
|
||||
internal static class AbCipMultiWritePlanner
|
||||
{
|
||||
/// <summary>
|
||||
/// One classified entry in the input batch. <see cref="OriginalIndex"/> preserves the
|
||||
/// caller's ordering so per-tag <c>StatusCode</c> fan-out lands at the right slot in
|
||||
/// the result array. <see cref="IsBitRmw"/> routes the entry through the RMW path even
|
||||
/// when the device supports packing.
|
||||
/// </summary>
|
||||
internal readonly record struct ClassifiedWrite(
|
||||
int OriginalIndex,
|
||||
WriteRequest Request,
|
||||
AbCipTagDefinition Definition,
|
||||
AbCipTagPath? ParsedPath,
|
||||
bool IsBitRmw);
|
||||
|
||||
/// <summary>
|
||||
/// One device's plan slice. <see cref="Packable"/> entries can be issued concurrently;
|
||||
/// <see cref="BitRmw"/> entries must go through the RMW path one-at-a-time per parent
|
||||
/// DINT.
|
||||
/// </summary>
|
||||
internal sealed class DevicePlan
|
||||
{
|
||||
public required string DeviceHostAddress { get; init; }
|
||||
public required AbCipPlcFamilyProfile Profile { get; init; }
|
||||
public List<ClassifiedWrite> Packable { get; } = new();
|
||||
public List<ClassifiedWrite> BitRmw { get; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <paramref name="reportPreflight"/> with the appropriate StatusCode and excluded from
|
||||
/// the plan.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<DevicePlan> Build(
|
||||
IReadOnlyList<WriteRequest> writes,
|
||||
IReadOnlyDictionary<string, AbCipTagDefinition> tagsByName,
|
||||
IReadOnlyDictionary<string, AbCipDriver.DeviceState> devices,
|
||||
Action<int, uint> reportPreflight)
|
||||
{
|
||||
var plans = new Dictionary<string, DevicePlan>(StringComparer.OrdinalIgnoreCase);
|
||||
var order = new List<DevicePlan>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user