Auto: ablegacy-9 — per-device timeout / retry overrides

Closes #252
This commit is contained in:
Joseph Doherty
2026-04-26 03:32:45 -04:00
parent 4ff1537d8a
commit c292dcc1db
9 changed files with 585 additions and 51 deletions

View File

@@ -209,6 +209,24 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
internal DeviceState? GetDeviceState(string hostAddress) =>
_devices.TryGetValue(hostAddress, out var s) ? s : null;
/// <summary>
/// PR 9 — per-device timeout precedence: device-level override wins, otherwise the
/// driver-wide default. Probe loop has its own timeout knob via
/// <see cref="AbLegacyProbeOptions.Timeout"/> but still falls back to the per-device
/// value when the probe override is absent (handled at the call site).
/// </summary>
internal TimeSpan ResolveTimeout(DeviceState device) =>
device.Options.Timeout ?? _options.Timeout;
/// <summary>
/// PR 9 — per-device retry count: device-level override wins, otherwise the driver-wide
/// default, otherwise zero (single attempt). The driver-wide default itself is
/// <c>null</c> by default so a vanilla AbLegacy config still issues exactly one read per
/// reference, matching pre-PR-9 behaviour.
/// </summary>
internal int ResolveRetries(DeviceState device) =>
device.Options.Retries ?? _options.Retries ?? 0;
// ---- IReadable ----
public async Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
@@ -232,57 +250,77 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
continue;
}
try
// PR 9 — per-device retry loop: on transient BadCommunicationError (libplctag throw
// OR a non-zero status that maps to BadCommunicationError) retry up to N times. A
// terminal mapped status (e.g. BadNodeIdUnknown for a missing PLC tag, BadTypeMismatch
// for a decoder mismatch) is surfaced as-is — retrying won't fix it. Cancellation
// always rethrows.
var retries = ResolveRetries(device);
DataValueSnapshot? snapshot = null;
for (var attempt = 0; attempt <= retries; attempt++)
{
var runtime = await EnsureTagRuntimeAsync(device, def, cancellationToken).ConfigureAwait(false);
await runtime.ReadAsync(cancellationToken).ConfigureAwait(false);
var status = runtime.GetStatus();
if (status != 0)
try
{
results[i] = new DataValueSnapshot(null,
AbLegacyStatusMapper.MapLibplctagStatus(status), null, now);
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead,
$"libplctag status {status} reading {reference}");
continue;
}
var runtime = await EnsureTagRuntimeAsync(device, def, cancellationToken).ConfigureAwait(false);
await runtime.ReadAsync(cancellationToken).ConfigureAwait(false);
var parsed = AbLegacyAddress.TryParse(def.Address, device.Options.PlcFamily);
// PR 7 — array contiguous block. Decode N consecutive elements via the runtime's
// per-index accessor and box the result as a typed .NET array. The parser has
// already rejected array+bit and array+sub-element combinations, so the array
// path can ignore the bit/sub-element decoders entirely.
int arrayCount;
if (parsed is not null && (def.ArrayLength is not null || (parsed.ArrayCount ?? 1) > 1))
{
arrayCount = ResolveElementCount(def, parsed);
}
else arrayCount = 1;
var status = runtime.GetStatus();
if (status != 0)
{
var mappedStatus = AbLegacyStatusMapper.MapLibplctagStatus(status);
// Transient: BadCommunicationError → eligible for retry.
if (mappedStatus == AbLegacyStatusMapper.BadCommunicationError && attempt < retries)
{
continue;
}
snapshot = new DataValueSnapshot(null, mappedStatus, null, now);
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead,
$"libplctag status {status} reading {reference}");
break;
}
if (arrayCount > 1)
{
var arr = DecodeArrayAs(runtime, def.DataType, arrayCount);
results[i] = new DataValueSnapshot(arr, AbLegacyStatusMapper.Good, now, now);
var parsed = AbLegacyAddress.TryParse(def.Address, device.Options.PlcFamily);
// PR 7 — array contiguous block. Decode N consecutive elements via the runtime's
// per-index accessor and box the result as a typed .NET array. The parser has
// already rejected array+bit and array+sub-element combinations, so the array
// path can ignore the bit/sub-element decoders entirely.
int arrayCount;
if (parsed is not null && (def.ArrayLength is not null || (parsed.ArrayCount ?? 1) > 1))
{
arrayCount = ResolveElementCount(def, parsed);
}
else arrayCount = 1;
if (arrayCount > 1)
{
var arr = DecodeArrayAs(runtime, def.DataType, arrayCount);
snapshot = new DataValueSnapshot(arr, AbLegacyStatusMapper.Good, now, now);
_health = new DriverHealth(DriverState.Healthy, now, null);
break;
}
// Timer/Counter/Control status bits route through GetBit at the parent-word
// address — translate the .DN/.EN/etc. sub-element to its standard bit position
// and pass it down to the runtime as a synthetic bitIndex.
var decodeBit = parsed?.BitIndex
?? AbLegacyDataTypeExtensions.StatusBitIndex(def.DataType, parsed?.SubElement);
var value = runtime.DecodeValue(def.DataType, decodeBit);
snapshot = new DataValueSnapshot(value, AbLegacyStatusMapper.Good, now, now);
_health = new DriverHealth(DriverState.Healthy, now, null);
continue;
break;
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
// Transient — exhaust retries before reporting BadCommunicationError.
if (attempt < retries) continue;
snapshot = new DataValueSnapshot(null,
AbLegacyStatusMapper.BadCommunicationError, null, now);
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ex.Message);
}
// Timer/Counter/Control status bits route through GetBit at the parent-word
// address — translate the .DN/.EN/etc. sub-element to its standard bit position
// and pass it down to the runtime as a synthetic bitIndex.
var decodeBit = parsed?.BitIndex
?? AbLegacyDataTypeExtensions.StatusBitIndex(def.DataType, parsed?.SubElement);
var value = runtime.DecodeValue(def.DataType, decodeBit);
results[i] = new DataValueSnapshot(value, AbLegacyStatusMapper.Good, now, now);
_health = new DriverHealth(DriverState.Healthy, now, null);
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
results[i] = new DataValueSnapshot(null,
AbLegacyStatusMapper.BadCommunicationError, null, now);
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ex.Message);
}
results[i] = snapshot ?? new DataValueSnapshot(null,
AbLegacyStatusMapper.BadCommunicationError, null, now);
}
return results;
@@ -441,13 +479,17 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
private async Task ProbeLoopAsync(DeviceState state, CancellationToken ct)
{
// PR 9 — per-device timeout wins over the probe's own timeout. Slow chassis (SLC 5/01
// RS-232 ~5 s round-trip) need their per-device override to flow into the probe too,
// otherwise the probe times out before the device ever has a chance to respond.
var probeTimeout = state.Options.Timeout ?? _options.Probe.Timeout;
var probeParams = new AbLegacyTagCreateParams(
Gateway: state.ParsedAddress.Gateway,
Port: state.ParsedAddress.Port,
CipPath: state.ParsedAddress.CipPath,
LibplctagPlcAttribute: state.Profile.LibplctagPlcAttribute,
TagName: _options.Probe.ProbeAddress!,
Timeout: _options.Probe.Timeout);
Timeout: probeTimeout);
IAbLegacyTagRuntime? probeRuntime = null;
while (!ct.IsCancellationRequested)
@@ -553,7 +595,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
CipPath: device.ParsedAddress.CipPath,
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
TagName: parentName,
Timeout: _options.Timeout));
Timeout: ResolveTimeout(device)));
try
{
await runtime.InitializeAsync(ct).ConfigureAwait(false);
@@ -601,7 +643,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
CipPath: device.ParsedAddress.CipPath,
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
TagName: tagName,
Timeout: _options.Timeout,
Timeout: ResolveTimeout(device),
ElementCount: elementCount));
try
{