@@ -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
|
||||
{
|
||||
|
||||
@@ -38,7 +38,10 @@ public static class AbLegacyDriverFactoryExtensions
|
||||
$"AB Legacy config for '{driverInstanceId}' has a device missing HostAddress"),
|
||||
PlcFamily: ParseEnum<AbLegacyPlcFamily>(d.PlcFamily, driverInstanceId, "PlcFamily",
|
||||
fallback: AbLegacyPlcFamily.Slc500),
|
||||
DeviceName: d.DeviceName))]
|
||||
DeviceName: d.DeviceName,
|
||||
// PR 9 — per-device timeout / retry overrides. Device-level wins over driver-wide.
|
||||
Timeout: d.TimeoutMs is int devMs ? TimeSpan.FromMilliseconds(devMs) : null,
|
||||
Retries: d.Retries))]
|
||||
: [],
|
||||
Tags = dto.Tags is { Count: > 0 }
|
||||
? [.. dto.Tags.Select(t => new AbLegacyTagDefinition(
|
||||
@@ -64,6 +67,9 @@ public static class AbLegacyDriverFactoryExtensions
|
||||
ProbeAddress = dto.Probe?.ProbeAddress ?? "S:0",
|
||||
},
|
||||
Timeout = TimeSpan.FromMilliseconds(dto.TimeoutMs ?? 2_000),
|
||||
// PR 9 — driver-wide retry default. null ≡ 0 retries (single attempt). Per-device
|
||||
// Retries on AbLegacyDeviceOptions still wins.
|
||||
Retries = dto.Retries,
|
||||
};
|
||||
|
||||
return new AbLegacyDriver(options, driverInstanceId);
|
||||
@@ -95,6 +101,12 @@ public static class AbLegacyDriverFactoryExtensions
|
||||
internal sealed class AbLegacyDriverConfigDto
|
||||
{
|
||||
public int? TimeoutMs { get; init; }
|
||||
/// <summary>
|
||||
/// PR 9 — driver-wide retry count for transient <c>BadCommunicationError</c> reads.
|
||||
/// <c>null</c> ≡ <c>0</c> (single attempt). A per-device override on
|
||||
/// <see cref="AbLegacyDeviceDto.Retries"/> wins.
|
||||
/// </summary>
|
||||
public int? Retries { get; init; }
|
||||
public List<AbLegacyDeviceDto>? Devices { get; init; }
|
||||
public List<AbLegacyTagDto>? Tags { get; init; }
|
||||
public AbLegacyProbeDto? Probe { get; init; }
|
||||
@@ -105,6 +117,20 @@ public static class AbLegacyDriverFactoryExtensions
|
||||
public string? HostAddress { get; init; }
|
||||
public string? PlcFamily { get; init; }
|
||||
public string? DeviceName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// PR 9 — optional per-device timeout in ms. Wins over the driver-wide
|
||||
/// <see cref="AbLegacyDriverConfigDto.TimeoutMs"/>. Tune this per chassis: SLC 5/01
|
||||
/// RS-232 ≈ 5000, SLC 5/05 ≈ 2000, MicroLogix 1100 ≈ 3000.
|
||||
/// </summary>
|
||||
public int? TimeoutMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// PR 9 — optional per-device retry count for transient <c>BadCommunicationError</c>
|
||||
/// reads. Wins over the driver-wide <see cref="AbLegacyDriverConfigDto.Retries"/>.
|
||||
/// <c>null</c> at both levels = single attempt.
|
||||
/// </summary>
|
||||
public int? Retries { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class AbLegacyTagDto
|
||||
|
||||
@@ -13,13 +13,35 @@ public sealed class AbLegacyDriverOptions
|
||||
public IReadOnlyList<AbLegacyDeviceOptions> Devices { get; init; } = [];
|
||||
public IReadOnlyList<AbLegacyTagDefinition> Tags { get; init; } = [];
|
||||
public AbLegacyProbeOptions Probe { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Driver-wide default per-operation timeout. Applies to every device unless that device
|
||||
/// overrides it via <see cref="AbLegacyDeviceOptions.Timeout"/> (PR 9).
|
||||
/// </summary>
|
||||
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
|
||||
|
||||
/// <summary>
|
||||
/// PR 9 — driver-wide default retry count for transient
|
||||
/// <c>BadCommunicationError</c> reads. <c>null</c> ≡ <c>0</c> (single attempt). Applies
|
||||
/// to every device unless that device overrides it via
|
||||
/// <see cref="AbLegacyDeviceOptions.Retries"/>.
|
||||
/// </summary>
|
||||
public int? Retries { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-device options for the AB Legacy driver. PR 9 added optional <see cref="Timeout"/>
|
||||
/// and <see cref="Retries"/> overrides — chassis families have very different per-operation
|
||||
/// latency floors (SLC 5/01 RS-232 ~5 s; SLC 5/05 ~2 s; ML1100 ~3 s) so a single driver-wide
|
||||
/// timeout always misfires on at least one device. Both fields are optional and fall back
|
||||
/// to the driver-wide default on <see cref="AbLegacyDriverOptions"/>.
|
||||
/// </summary>
|
||||
public sealed record AbLegacyDeviceOptions(
|
||||
string HostAddress,
|
||||
AbLegacyPlcFamily PlcFamily = AbLegacyPlcFamily.Slc500,
|
||||
string? DeviceName = null);
|
||||
string? DeviceName = null,
|
||||
TimeSpan? Timeout = null,
|
||||
int? Retries = null);
|
||||
|
||||
/// <summary>
|
||||
/// One PCCC-backed OPC UA variable. <c>Address</c> is the canonical PCCC file-address
|
||||
|
||||
Reference in New Issue
Block a user