@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user