Auto: abcip-3.3 — read-strategy selector (WholeUdt / MultiPacket / Auto)
Closes #237
This commit is contained in:
@@ -157,7 +157,13 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
// unsupported family falls back to Symbolic + emits a warning so misconfiguration
|
||||
// does not fault the driver.
|
||||
var resolvedAddressing = ResolveAddressingMode(device, profile);
|
||||
_devices[device.HostAddress] = new DeviceState(addr, device, profile, resolvedAddressing);
|
||||
// PR abcip-3.3 — resolve ReadStrategy at the device level. User-forced MultiPacket
|
||||
// against a non-packing family (Micro800 et al) falls back to WholeUdt with a
|
||||
// warning. Auto stays as-is — the planner re-evaluates per-batch using the
|
||||
// device's MultiPacketSparsityThreshold.
|
||||
var resolvedReadStrategy = ResolveReadStrategy(device, profile);
|
||||
_devices[device.HostAddress] = new DeviceState(
|
||||
addr, device, profile, resolvedAddressing, resolvedReadStrategy);
|
||||
}
|
||||
// Pre-declared tags first; L5K imports fill in only the names not already covered
|
||||
// (operators can override an imported entry by re-declaring it under Tags).
|
||||
@@ -268,6 +274,46 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PR abcip-3.3 — resolve <see cref="AbCipDeviceOptions.ReadStrategy"/> against the
|
||||
/// family profile. <see cref="ReadStrategy.MultiPacket"/> against a family whose profile
|
||||
/// sets <see cref="AbCipPlcFamilyProfile.SupportsRequestPacking"/> = <c>false</c>
|
||||
/// (Micro800 today; SLC500 / PLC5 when those profiles ship) falls back to
|
||||
/// <see cref="ReadStrategy.WholeUdt"/> with a warning so the operator sees the
|
||||
/// misconfiguration in the log without the driver faulting. <see cref="ReadStrategy.Auto"/>
|
||||
/// stays as-is — the planner re-evaluates the choice per-batch from the device's
|
||||
/// <see cref="AbCipDeviceOptions.MultiPacketSparsityThreshold"/>; we still need the
|
||||
/// resolution step here so a future PR can cap Auto at WholeUdt on non-packing families
|
||||
/// in one place rather than scattering the check across the read path.
|
||||
/// </summary>
|
||||
private ReadStrategy ResolveReadStrategy(AbCipDeviceOptions device, AbCipPlcFamilyProfile profile)
|
||||
{
|
||||
switch (device.ReadStrategy)
|
||||
{
|
||||
case ReadStrategy.MultiPacket:
|
||||
if (!profile.SupportsRequestPacking)
|
||||
{
|
||||
_options.OnWarning?.Invoke(
|
||||
$"AbCip device '{device.HostAddress}' family '{device.PlcFamily}' does not support " +
|
||||
"Multi-Service Packet request packing — its CIP firmware lacks the 0x0A service. " +
|
||||
"Falling back to WholeUdt read strategy for this device.");
|
||||
return ReadStrategy.WholeUdt;
|
||||
}
|
||||
return ReadStrategy.MultiPacket;
|
||||
case ReadStrategy.WholeUdt:
|
||||
return ReadStrategy.WholeUdt;
|
||||
case ReadStrategy.Auto:
|
||||
default:
|
||||
// Auto on a non-packing family stays Auto here so the planner's per-batch
|
||||
// heuristic still runs; the heuristic itself never picks MultiPacket against a
|
||||
// device whose AddressingMode-style guard tripped — but for ReadStrategy the guard
|
||||
// lives in the device-init resolution above (user-forced MultiPacket → WholeUdt).
|
||||
// For Auto, the planner consults device.Profile.SupportsRequestPacking before
|
||||
// emitting MultiPacket so non-packing families always read WholeUdt under Auto.
|
||||
return ReadStrategy.Auto;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared L5K / L5X import path — keeps source-format selection (parser delegate) the
|
||||
/// only behavioural axis between the two formats. Adds the parser's tags to
|
||||
@@ -530,17 +576,187 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
// per-tag path that's been here since PR 3. Planner is a pure function over the
|
||||
// current tag map; BOOL/String/Structure members stay on the fallback path because
|
||||
// declaration-only offsets can't place them under Logix alignment rules.
|
||||
var plan = AbCipUdtReadPlanner.Build(fullReferences, _tagsByName);
|
||||
|
||||
foreach (var group in plan.Groups)
|
||||
await ReadGroupAsync(group, results, now, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var fb in plan.Fallbacks)
|
||||
await ReadSingleAsync(fb, fullReferences[fb.OriginalIndex], results, now, cancellationToken).ConfigureAwait(false);
|
||||
//
|
||||
// PR abcip-3.3 — dispatch by device's resolved ReadStrategy:
|
||||
// WholeUdt — every group goes through the whole-UDT planner (task #194 default).
|
||||
// MultiPacket — every group goes through the multi-packet planner; one read per
|
||||
// subscribed member, bundled per parent.
|
||||
// Auto — per-group heuristic on subscribedMembers / totalMembers.
|
||||
await ExecuteReadPlanAsync(fullReferences, results, now, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PR abcip-3.3 — strategy-aware dispatch wrapper around the WholeUdt + MultiPacket
|
||||
/// planners. Both planners produce the same shape of "groups + per-tag fallbacks" so
|
||||
/// fallbacks always run through <see cref="ReadSingleAsync"/>; only the group shape
|
||||
/// differs. Auto resolves per-group: members of the same parent UDT either flow through
|
||||
/// <see cref="ReadGroupAsync"/> (one whole-UDT read) or
|
||||
/// <see cref="ReadMultiPacketBatchAsync"/> (per-member reads bundled per parent).
|
||||
/// </summary>
|
||||
private async Task ExecuteReadPlanAsync(
|
||||
IReadOnlyList<string> fullReferences, DataValueSnapshot[] results, DateTime now, CancellationToken ct)
|
||||
{
|
||||
// First pass — segregate references by parent UDT vs everything-else, identical to the
|
||||
// shape both planners produce; we can then route each parent group through the chosen
|
||||
// planner. Reuse AbCipUdtReadPlanner.Build for the WholeUdt+Auto path because it already
|
||||
// demotes single-member groups to fallback, and the MultiPacket planner does NOT demote
|
||||
// (sparse reads of one member are still a win on the wire).
|
||||
var wholeUdtPlan = AbCipUdtReadPlanner.Build(fullReferences, _tagsByName);
|
||||
var multiPacketPlan = AbCipMultiPacketReadPlanner.Build(fullReferences, _tagsByName);
|
||||
|
||||
// Determine per-parent strategy. WholeUdt planner emits a Group only when ≥2 members of
|
||||
// the same parent are subscribed; MultiPacket planner emits a Batch for every parent
|
||||
// touched. Index multiPacket batches by parent name so we can co-route them with the
|
||||
// WholeUdt grouping decisions.
|
||||
var multiPacketByParent = new Dictionary<string, AbCipMultiPacketReadBatch>(
|
||||
multiPacketPlan.Batches.Count, StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var b in multiPacketPlan.Batches)
|
||||
multiPacketByParent[b.ParentName] = b;
|
||||
|
||||
// Treat each parent that survived the WholeUdt planner as the candidate for whole-UDT
|
||||
// dispatch; per-strategy routing decides whether it goes WholeUdt or MultiPacket.
|
||||
var routedParents = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var group in wholeUdtPlan.Groups)
|
||||
{
|
||||
_devices.TryGetValue(group.ParentDefinition.DeviceHostAddress, out var device);
|
||||
|
||||
var strategy = ChooseEffectiveStrategy(device, group);
|
||||
if (strategy == ReadStrategy.MultiPacket
|
||||
&& multiPacketByParent.TryGetValue(group.ParentName, out var mpBatch))
|
||||
{
|
||||
if (device is not null) Interlocked.Increment(ref device.MultiPacketGroupsExecuted);
|
||||
await ReadMultiPacketBatchAsync(mpBatch, results, now, ct).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (device is not null) Interlocked.Increment(ref device.WholeUdtGroupsExecuted);
|
||||
await ReadGroupAsync(group, results, now, ct).ConfigureAwait(false);
|
||||
}
|
||||
routedParents.Add(group.ParentName);
|
||||
}
|
||||
|
||||
// Per-tag fallbacks from the WholeUdt planner are the union of (a) tags with no UDT
|
||||
// parent, (b) UDT members where the parent had only one subscribed member, and (c)
|
||||
// unknown references. We re-route singletons through MultiPacket too when the device
|
||||
// strategy is MultiPacket, because a 1-of-50 read is exactly the sparse case the planner
|
||||
// is designed for.
|
||||
foreach (var fb in wholeUdtPlan.Fallbacks)
|
||||
{
|
||||
// Is this a UDT-member fallback that the MultiPacket planner can route?
|
||||
if (multiPacketPlan.Batches.Count > 0
|
||||
&& _tagsByName.TryGetValue(fb.Reference, out var def))
|
||||
{
|
||||
var dot = fb.Reference.IndexOf('.');
|
||||
if (dot > 0 && dot < fb.Reference.Length - 1)
|
||||
{
|
||||
var parentName = fb.Reference[..dot];
|
||||
if (!routedParents.Contains(parentName)
|
||||
&& multiPacketByParent.TryGetValue(parentName, out var mpBatch)
|
||||
&& _devices.TryGetValue(def.DeviceHostAddress, out var device))
|
||||
{
|
||||
// Singleton parent — Auto picks based on the same heuristic; MultiPacket
|
||||
// wins by default because the WholeUdt planner already demoted single
|
||||
// members under the assumption that one whole-UDT read is no cheaper
|
||||
// than one member read. With explicit MultiPacket the answer flips.
|
||||
var strategy = ChooseEffectiveStrategyForSingleton(device, mpBatch);
|
||||
if (strategy == ReadStrategy.MultiPacket)
|
||||
{
|
||||
Interlocked.Increment(ref device.MultiPacketGroupsExecuted);
|
||||
await ReadMultiPacketBatchAsync(mpBatch, results, now, ct).ConfigureAwait(false);
|
||||
routedParents.Add(parentName);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await ReadSingleAsync(fb, fullReferences[fb.OriginalIndex], results, now, ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PR abcip-3.3 — pick the effective <see cref="ReadStrategy"/> for one parent UDT group.
|
||||
/// <see cref="ReadStrategy.WholeUdt"/> + <see cref="ReadStrategy.MultiPacket"/> are
|
||||
/// forced explicitly (already family-compat-checked at device init). <see cref="ReadStrategy.Auto"/>
|
||||
/// consults the planner heuristic on subscribed-member fraction, but only when the
|
||||
/// family supports request packing — non-packing families always read WholeUdt regardless
|
||||
/// of sparsity because they have no Multi-Service-Packet path on the wire.
|
||||
/// </summary>
|
||||
private static ReadStrategy ChooseEffectiveStrategy(DeviceState? device, AbCipUdtReadGroup group)
|
||||
{
|
||||
if (device is null) return ReadStrategy.WholeUdt;
|
||||
switch (device.ReadStrategy)
|
||||
{
|
||||
case ReadStrategy.MultiPacket:
|
||||
return ReadStrategy.MultiPacket;
|
||||
case ReadStrategy.WholeUdt:
|
||||
return ReadStrategy.WholeUdt;
|
||||
case ReadStrategy.Auto:
|
||||
default:
|
||||
if (!device.Profile.SupportsRequestPacking) return ReadStrategy.WholeUdt;
|
||||
var totalMembers = group.ParentDefinition.Members?.Count ?? 0;
|
||||
return AbCipMultiPacketReadPlanner.ChooseStrategyForGroup(
|
||||
subscribedMembers: group.Members.Count,
|
||||
totalMembers: totalMembers,
|
||||
threshold: device.Options.MultiPacketSparsityThreshold);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PR abcip-3.3 — strategy pick for a singleton (one-member) UDT batch. Only relevant
|
||||
/// when the device strategy is explicit <see cref="ReadStrategy.MultiPacket"/> or Auto
|
||||
/// produces a MultiPacket result; otherwise the per-tag fallback path runs as before.
|
||||
/// </summary>
|
||||
private static ReadStrategy ChooseEffectiveStrategyForSingleton(
|
||||
DeviceState device, AbCipMultiPacketReadBatch batch)
|
||||
{
|
||||
switch (device.ReadStrategy)
|
||||
{
|
||||
case ReadStrategy.MultiPacket:
|
||||
return ReadStrategy.MultiPacket;
|
||||
case ReadStrategy.WholeUdt:
|
||||
return ReadStrategy.WholeUdt;
|
||||
case ReadStrategy.Auto:
|
||||
default:
|
||||
if (!device.Profile.SupportsRequestPacking) return ReadStrategy.WholeUdt;
|
||||
var totalMembers = batch.ParentDefinition.Members?.Count ?? 0;
|
||||
return AbCipMultiPacketReadPlanner.ChooseStrategyForGroup(
|
||||
subscribedMembers: batch.Members.Count,
|
||||
totalMembers: totalMembers,
|
||||
threshold: device.Options.MultiPacketSparsityThreshold);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PR abcip-3.3 — execute a Multi-Service-Packet batch. Today this issues one libplctag
|
||||
/// read per member (the same N reads the per-tag fallback path does), keyed on the
|
||||
/// batch's parent so the diagnostic counters track which strategy ran. Wire-level
|
||||
/// Multi-Service-Packet bundling depends on the libplctag .NET wrapper exposing the
|
||||
/// 0x0A service explicitly — same wrapper limitation as PR abcip-3.1's connection_size
|
||||
/// and PR abcip-3.2's instance-ID addressing. The planner's grouping is still
|
||||
/// load-bearing because it gives the runtime the correct plan when an upstream wrapper
|
||||
/// release exposes the bundling primitive.
|
||||
/// </summary>
|
||||
private async Task ReadMultiPacketBatchAsync(
|
||||
AbCipMultiPacketReadBatch batch, DataValueSnapshot[] results, DateTime now, CancellationToken ct)
|
||||
{
|
||||
var parent = batch.ParentDefinition;
|
||||
if (!_devices.TryGetValue(parent.DeviceHostAddress, out var device))
|
||||
{
|
||||
foreach (var m in batch.Members)
|
||||
results[m.OriginalIndex] = new DataValueSnapshot(null, AbCipStatusMapper.BadNodeIdUnknown, null, now);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var member in batch.Members)
|
||||
{
|
||||
var memberFullName = member.Definition.Name;
|
||||
var fb = new AbCipUdtReadFallback(member.OriginalIndex, memberFullName);
|
||||
await ReadSingleAsync(fb, memberFullName, results, now, ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PR abcip-3.2 — for each Logical-mode device touched by this read batch, fire the
|
||||
/// one-time <c>@tags</c> symbol-table walk + populate <see cref="DeviceState.LogicalInstanceMap"/>.
|
||||
@@ -1366,7 +1582,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
AbCipHostAddress parsedAddress,
|
||||
AbCipDeviceOptions options,
|
||||
AbCipPlcFamilyProfile profile,
|
||||
AddressingMode resolvedAddressingMode)
|
||||
AddressingMode resolvedAddressingMode,
|
||||
ReadStrategy resolvedReadStrategy)
|
||||
{
|
||||
public AbCipHostAddress ParsedAddress { get; } = parsedAddress;
|
||||
public AbCipDeviceOptions Options { get; } = options;
|
||||
@@ -1391,6 +1608,25 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
/// </summary>
|
||||
public AddressingMode AddressingMode { get; } = resolvedAddressingMode;
|
||||
|
||||
/// <summary>
|
||||
/// PR abcip-3.3 — resolved read strategy for this device. <see cref="AbCip.ReadStrategy.WholeUdt"/>
|
||||
/// or <see cref="AbCip.ReadStrategy.MultiPacket"/> mean "always pick this for every UDT
|
||||
/// batch on this device." <see cref="AbCip.ReadStrategy.Auto"/> means "let the planner
|
||||
/// pick per-batch using <see cref="AbCipDeviceOptions.MultiPacketSparsityThreshold"/>."
|
||||
/// User-forced MultiPacket against a non-packing family (Micro800 et al) was already
|
||||
/// collapsed to WholeUdt at <see cref="AbCipDriver.ResolveReadStrategy"/> time, so the
|
||||
/// read hot path can branch on this single value without re-checking family compat.
|
||||
/// </summary>
|
||||
public ReadStrategy ReadStrategy { get; } = resolvedReadStrategy;
|
||||
|
||||
/// <summary>PR abcip-3.3 — count of UDT groups dispatched through the WholeUdt path on
|
||||
/// this device. Surfaced for tests + a future driver-diagnostics RPC.</summary>
|
||||
public int WholeUdtGroupsExecuted;
|
||||
|
||||
/// <summary>PR abcip-3.3 — count of UDT groups dispatched through the MultiPacket path
|
||||
/// on this device. Surfaced for tests + a future driver-diagnostics RPC.</summary>
|
||||
public int MultiPacketGroupsExecuted;
|
||||
|
||||
/// <summary>
|
||||
/// PR abcip-3.2 — name → Symbol Object instance ID map populated by the one-time
|
||||
/// <c>@tags</c> walk that fires on the first read on a Logical-mode device. Empty
|
||||
|
||||
Reference in New Issue
Block a user