fix(driver-abcip): resolve Low code-review findings (Driver.AbCip-007,011,012,013,015)
- Driver.AbCip-007: inject an optional ILogger<AbCipDriver> / ILogger<AbCipAlarmProjection> (default NullLogger) and log around every read / write / template-fetch / probe / alarm-poll failure path. - Driver.AbCip-011: LogWarning when InitializeAsync is configured with Probe.Enabled=true but ProbeTagPath is blank — operators now see why GetHostStatuses keeps reporting Unknown. - Driver.AbCip-012: documented the LibplctagTemplateReader per-call Tag cost as accepted given libplctag's own connection pool and the low-frequency discovery use-case. - Driver.AbCip-013: per-device AllowPacking + ConnectionSize overrides on AbCipDeviceOptions, threaded through AbCipTagCreateParams; central BuildCreateParams helper replaces five ad-hoc clones; AllowPacking now reaches Tag.AllowPacking at runtime. - Driver.AbCip-015: stale-comment sweep — every PR-N forward-reference is rewritten to describe present behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
@@ -32,14 +34,16 @@ internal sealed class AbCipAlarmProjection : IAsyncDisposable
|
||||
{
|
||||
private readonly AbCipDriver _driver;
|
||||
private readonly TimeSpan _pollInterval;
|
||||
private readonly ILogger _logger;
|
||||
private readonly Dictionary<long, Subscription> _subs = new();
|
||||
private readonly Lock _subsLock = new();
|
||||
private long _nextId;
|
||||
|
||||
public AbCipAlarmProjection(AbCipDriver driver, TimeSpan pollInterval)
|
||||
public AbCipAlarmProjection(AbCipDriver driver, TimeSpan pollInterval, ILogger? logger = null)
|
||||
{
|
||||
_driver = driver;
|
||||
_pollInterval = pollInterval;
|
||||
_logger = logger ?? NullLogger.Instance;
|
||||
}
|
||||
|
||||
public async Task<IAlarmSubscriptionHandle> SubscribeAsync(
|
||||
@@ -158,7 +162,14 @@ internal sealed class AbCipAlarmProjection : IAsyncDisposable
|
||||
Tick(sub, results);
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested) { break; }
|
||||
catch { /* per-tick failures are non-fatal; next tick retries */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Per-tick failures are non-fatal; next tick retries. Log at debug because a
|
||||
// wedged controller produces one exception per tick and the operator already
|
||||
// sees the failed-read warning from ReadAsync below this layer; this log just
|
||||
// confirms the alarm projection loop is still running.
|
||||
_logger.LogDebug(ex, "AbCip alarm-projection poll tick failed (will retry)");
|
||||
}
|
||||
|
||||
try { await Task.Delay(_pollInterval, ct).ConfigureAwait(false); }
|
||||
catch (OperationCanceledException) { break; }
|
||||
|
||||
@@ -5,7 +5,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
/// <summary>
|
||||
/// Logix atomic + string data types, plus a <see cref="Structure"/> marker used when a tag
|
||||
/// references a UDT / predefined structure (Timer, Counter, Control). The concrete UDT
|
||||
/// shape is resolved via the CIP Template Object at discovery time (PR 5 / PR 6).
|
||||
/// shape is resolved via the CIP Template Object at discovery time (see
|
||||
/// <see cref="CipTemplateObjectDecoder"/> + <see cref="AbCipTemplateCache"/>).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Mirrors the shape of <c>ModbusDataType</c>. Atomic Logix names (BOOL / SINT / INT / DINT /
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbCip.PlcFamilies;
|
||||
|
||||
@@ -34,6 +36,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
private readonly PollGroupEngine _poll;
|
||||
private readonly Dictionary<string, DeviceState> _devices = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, AbCipTagDefinition> _tagsByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ILogger<AbCipDriver> _logger;
|
||||
private AbCipAlarmProjection _alarmProjection;
|
||||
private DriverHealth _health = new(DriverState.Unknown, null, null);
|
||||
|
||||
@@ -47,7 +50,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
public AbCipDriver(AbCipDriverOptions options, string driverInstanceId,
|
||||
IAbCipTagFactory? tagFactory = null,
|
||||
IAbCipTagEnumeratorFactory? enumeratorFactory = null,
|
||||
IAbCipTemplateReaderFactory? templateReaderFactory = null)
|
||||
IAbCipTemplateReaderFactory? templateReaderFactory = null,
|
||||
ILogger<AbCipDriver>? logger = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
_options = options;
|
||||
@@ -55,11 +59,12 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
_tagFactory = tagFactory ?? new LibplctagTagFactory();
|
||||
_enumeratorFactory = enumeratorFactory ?? new LibplctagTagEnumeratorFactory();
|
||||
_templateReaderFactory = templateReaderFactory ?? new LibplctagTemplateReaderFactory();
|
||||
_logger = logger ?? NullLogger<AbCipDriver>.Instance;
|
||||
_poll = new PollGroupEngine(
|
||||
reader: ReadAsync,
|
||||
onChange: (handle, tagRef, snapshot) =>
|
||||
OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, tagRef, snapshot)));
|
||||
_alarmProjection = new AbCipAlarmProjection(this, _options.AlarmPollInterval);
|
||||
_alarmProjection = new AbCipAlarmProjection(this, _options.AlarmPollInterval, _logger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -77,13 +82,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
|
||||
if (!_devices.TryGetValue(deviceHostAddress, out var device)) return null;
|
||||
|
||||
var deviceParams = new AbCipTagCreateParams(
|
||||
Gateway: device.ParsedAddress.Gateway,
|
||||
Port: device.ParsedAddress.Port,
|
||||
CipPath: device.ParsedAddress.CipPath,
|
||||
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
||||
TagName: $"@udt/{templateInstanceId}",
|
||||
Timeout: _options.Timeout);
|
||||
var deviceParams = device.BuildCreateParams($"@udt/{templateInstanceId}", _options.Timeout);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -95,16 +94,23 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
return shape;
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Template read failure — log via the driver's health surface so operators see it,
|
||||
// but don't propagate since callers should fall back to declaration-driven UDT
|
||||
// semantics rather than failing the whole discovery run.
|
||||
// Template read failure — surface via the driver's health surface AND a warning
|
||||
// log so operators see it; don't propagate since callers should fall back to
|
||||
// declaration-driven UDT semantics rather than failing the whole discovery run.
|
||||
_logger.LogWarning(ex,
|
||||
"AbCip driver {DriverInstanceId} failed to read UDT template {TemplateInstanceId} from device {Device}; " +
|
||||
"falling back to declaration-driven UDT semantics",
|
||||
_driverInstanceId, templateInstanceId, deviceHostAddress);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Shared UDT template cache. Exposed for PR 6 (UDT reader) + diagnostics.</summary>
|
||||
/// <summary>
|
||||
/// Shared UDT template cache populated by <see cref="FetchUdtShapeAsync"/>. Exposed
|
||||
/// internally so tests + diagnostics can inspect cached shapes.
|
||||
/// </summary>
|
||||
internal AbCipTemplateCache TemplateCache => _templateCache;
|
||||
|
||||
public string DriverInstanceId => _driverInstanceId;
|
||||
@@ -132,7 +138,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
if (parsed.Devices.Count > 0 || parsed.Tags.Count > 0)
|
||||
{
|
||||
_options = parsed;
|
||||
_alarmProjection = new AbCipAlarmProjection(this, _options.AlarmPollInterval);
|
||||
_alarmProjection = new AbCipAlarmProjection(this, _options.AlarmPollInterval, _logger);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,11 +198,24 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
state.ProbeTask = Task.Run(() => ProbeLoopAsync(state, ct), ct);
|
||||
}
|
||||
}
|
||||
else if (_options.Probe.Enabled && _devices.Count > 0)
|
||||
{
|
||||
// Driver.AbCip-011: probe is Enabled but no ProbeTagPath is configured. Without a
|
||||
// tag path the loop has nothing to read, so HostState would stay Unknown forever
|
||||
// and GetHostStatuses() would report every device as Unknown with no warning.
|
||||
// Log a warning so the misconfiguration is visible in the rolling Serilog file.
|
||||
_logger.LogWarning(
|
||||
"AbCip probe is enabled but no ProbeTagPath is configured for driver {DriverInstanceId} — " +
|
||||
"host connectivity probe loops were NOT started; GetHostStatuses() will report every device " +
|
||||
"as Unknown until a ProbeTagPath is set or Probe.Enabled is set to false.",
|
||||
_driverInstanceId);
|
||||
}
|
||||
_health = new DriverHealth(DriverState.Healthy, DateTime.UtcNow, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_health = new DriverHealth(DriverState.Faulted, null, ex.Message);
|
||||
_logger.LogError(ex, "AbCip driver {DriverInstanceId} failed to initialize", _driverInstanceId);
|
||||
throw;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
@@ -307,13 +326,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
|
||||
private async Task ProbeLoopAsync(DeviceState state, CancellationToken ct)
|
||||
{
|
||||
var probeParams = new AbCipTagCreateParams(
|
||||
Gateway: state.ParsedAddress.Gateway,
|
||||
Port: state.ParsedAddress.Port,
|
||||
CipPath: state.ParsedAddress.CipPath,
|
||||
LibplctagPlcAttribute: state.Profile.LibplctagPlcAttribute,
|
||||
TagName: _options.Probe.ProbeTagPath!,
|
||||
Timeout: _options.Probe.Timeout);
|
||||
var probeParams = state.BuildCreateParams(_options.Probe.ProbeTagPath!, _options.Probe.Timeout);
|
||||
|
||||
IAbCipTagRuntime? probeRuntime = null;
|
||||
while (!ct.IsCancellationRequested)
|
||||
@@ -336,9 +349,14 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Wire / init error — tear down the probe runtime so the next tick re-creates it.
|
||||
// Log at debug because a wedged device produces one per tick; the
|
||||
// OnHostStatusChanged event is the persistent record once the state transitions.
|
||||
_logger.LogDebug(ex,
|
||||
"AbCip probe tick failed for driver {DriverInstanceId} device {Device}",
|
||||
_driverInstanceId, state.Options.HostAddress);
|
||||
try { probeRuntime?.Dispose(); } catch { }
|
||||
probeRuntime = null;
|
||||
state.ProbeInitialized = false;
|
||||
@@ -402,7 +420,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
|
||||
// Task #194 — plan the batch: members of the same parent UDT get collapsed into one
|
||||
// whole-UDT read + in-memory member decode; every other reference falls back to the
|
||||
// per-tag path that's been here since PR 3. Planner is a pure function over the
|
||||
// per-tag read path. 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. Whole-UDT
|
||||
// grouping is itself gated behind EnableDeclarationOnlyUdtGrouping — Studio 5000 may
|
||||
@@ -460,6 +478,10 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
AbCipStatusMapper.MapLibplctagStatus(status), null, now);
|
||||
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead,
|
||||
$"libplctag status {status} reading {reference}");
|
||||
_logger.LogWarning(
|
||||
"AbCip read returned non-zero libplctag status {LibplctagStatus} for tag {Tag} on device {Device}; " +
|
||||
"evicting cached runtime so next call re-creates it",
|
||||
status, reference, def.DeviceHostAddress);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -480,6 +502,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
results[fb.OriginalIndex] = new DataValueSnapshot(null,
|
||||
AbCipStatusMapper.BadCommunicationError, null, now);
|
||||
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ex.Message);
|
||||
_logger.LogWarning(ex,
|
||||
"AbCip read transport exception for tag {Tag} on device {Device}",
|
||||
reference, def.DeviceHostAddress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -514,6 +539,10 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
StampGroupStatus(group, results, now, mapped);
|
||||
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead,
|
||||
$"libplctag status {status} reading UDT {group.ParentName}");
|
||||
_logger.LogWarning(
|
||||
"AbCip whole-UDT read returned non-zero libplctag status {LibplctagStatus} for parent {Parent} " +
|
||||
"on device {Device}; {MemberCount} member values stamped with mapped status",
|
||||
status, group.ParentName, parent.DeviceHostAddress, group.Members.Count);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -533,6 +562,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
EvictRuntime(device, parent.Name); // Driver.AbCip-010
|
||||
StampGroupStatus(group, results, now, AbCipStatusMapper.BadCommunicationError);
|
||||
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ex.Message);
|
||||
_logger.LogWarning(ex,
|
||||
"AbCip whole-UDT read transport exception for parent {Parent} on device {Device}",
|
||||
group.ParentName, parent.DeviceHostAddress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,6 +637,10 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
{
|
||||
EvictRuntime(device, def.Name); // Driver.AbCip-010
|
||||
results[i] = new WriteResult(AbCipStatusMapper.MapLibplctagStatus(status));
|
||||
_logger.LogWarning(
|
||||
"AbCip write returned non-zero libplctag status {LibplctagStatus} for tag {Tag} on device {Device}; " +
|
||||
"evicting cached runtime so next call re-creates it",
|
||||
status, w.FullReference, def.DeviceHostAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -621,22 +657,34 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
// Type/protocol error — not a transport fault; don't evict the handle.
|
||||
results[i] = new WriteResult(AbCipStatusMapper.BadNotSupported);
|
||||
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, nse.Message);
|
||||
_logger.LogWarning(nse,
|
||||
"AbCip write not supported for tag {Tag} on device {Device}",
|
||||
w.FullReference, def.DeviceHostAddress);
|
||||
}
|
||||
catch (FormatException fe)
|
||||
{
|
||||
// Value conversion error — not a transport fault; don't evict.
|
||||
results[i] = new WriteResult(AbCipStatusMapper.BadTypeMismatch);
|
||||
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, fe.Message);
|
||||
_logger.LogWarning(fe,
|
||||
"AbCip write value-conversion error for tag {Tag} on device {Device}",
|
||||
w.FullReference, def.DeviceHostAddress);
|
||||
}
|
||||
catch (InvalidCastException ice)
|
||||
{
|
||||
results[i] = new WriteResult(AbCipStatusMapper.BadTypeMismatch);
|
||||
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ice.Message);
|
||||
_logger.LogWarning(ice,
|
||||
"AbCip write type-cast error for tag {Tag} on device {Device}",
|
||||
w.FullReference, def.DeviceHostAddress);
|
||||
}
|
||||
catch (OverflowException oe)
|
||||
{
|
||||
results[i] = new WriteResult(AbCipStatusMapper.BadOutOfRange);
|
||||
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, oe.Message);
|
||||
_logger.LogWarning(oe,
|
||||
"AbCip write value out of range for tag {Tag} on device {Device}",
|
||||
w.FullReference, def.DeviceHostAddress);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -644,6 +692,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
EvictRuntime(device, def.Name); // Driver.AbCip-010
|
||||
results[i] = new WriteResult(AbCipStatusMapper.BadCommunicationError);
|
||||
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ex.Message);
|
||||
_logger.LogWarning(ex,
|
||||
"AbCip write transport exception for tag {Tag} on device {Device}",
|
||||
w.FullReference, def.DeviceHostAddress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -698,13 +749,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
{
|
||||
if (device.ParentRuntimes.TryGetValue(parentTagName, out var existing)) return existing;
|
||||
|
||||
var runtime = _tagFactory.Create(new AbCipTagCreateParams(
|
||||
Gateway: device.ParsedAddress.Gateway,
|
||||
Port: device.ParsedAddress.Port,
|
||||
CipPath: device.ParsedAddress.CipPath,
|
||||
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
||||
TagName: parentTagName,
|
||||
Timeout: _options.Timeout));
|
||||
var runtime = _tagFactory.Create(device.BuildCreateParams(parentTagName, _options.Timeout));
|
||||
try
|
||||
{
|
||||
await runtime.InitializeAsync(ct).ConfigureAwait(false);
|
||||
@@ -736,13 +781,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
?? throw new InvalidOperationException(
|
||||
$"AbCip tag '{def.Name}' has malformed TagPath '{def.TagPath}'.");
|
||||
|
||||
var runtime = _tagFactory.Create(new AbCipTagCreateParams(
|
||||
Gateway: device.ParsedAddress.Gateway,
|
||||
Port: device.ParsedAddress.Port,
|
||||
CipPath: device.ParsedAddress.CipPath,
|
||||
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
||||
TagName: parsed.ToLibplctagName(),
|
||||
Timeout: _options.Timeout));
|
||||
var runtime = _tagFactory.Create(device.BuildCreateParams(parsed.ToLibplctagName(), _options.Timeout));
|
||||
try
|
||||
{
|
||||
await runtime.InitializeAsync(ct).ConfigureAwait(false);
|
||||
@@ -850,13 +889,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
if (_options.EnableControllerBrowse && _devices.TryGetValue(device.HostAddress, out var state))
|
||||
{
|
||||
using var enumerator = _enumeratorFactory.Create();
|
||||
var deviceParams = new AbCipTagCreateParams(
|
||||
Gateway: state.ParsedAddress.Gateway,
|
||||
Port: state.ParsedAddress.Port,
|
||||
CipPath: state.ParsedAddress.CipPath,
|
||||
LibplctagPlcAttribute: state.Profile.LibplctagPlcAttribute,
|
||||
TagName: "@tags",
|
||||
Timeout: _options.Timeout);
|
||||
var deviceParams = state.BuildCreateParams("@tags", _options.Timeout);
|
||||
|
||||
IAddressSpaceBuilder? discoveredFolder = null;
|
||||
await foreach (var discovered in enumerator.EnumerateAsync(deviceParams, cancellationToken)
|
||||
@@ -966,6 +999,23 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
public SemaphoreSlim GetRmwLock(string parentTagName) =>
|
||||
_rmwLocks.GetOrAdd(parentTagName, _ => new SemaphoreSlim(1, 1));
|
||||
|
||||
/// <summary>
|
||||
/// Driver.AbCip-013 — compute the effective <see cref="AbCipTagCreateParams"/> for a
|
||||
/// tag on this device. Combines the per-device options
|
||||
/// (<see cref="AbCipDeviceOptions.AllowPacking"/>,
|
||||
/// <see cref="AbCipDeviceOptions.ConnectionSize"/>) with the family profile defaults
|
||||
/// so the wire layer sees one place that resolves both.
|
||||
/// </summary>
|
||||
public AbCipTagCreateParams BuildCreateParams(string tagName, TimeSpan timeout) => new(
|
||||
Gateway: ParsedAddress.Gateway,
|
||||
Port: ParsedAddress.Port,
|
||||
CipPath: ParsedAddress.CipPath,
|
||||
LibplctagPlcAttribute: Profile.LibplctagPlcAttribute,
|
||||
TagName: tagName,
|
||||
Timeout: timeout,
|
||||
AllowPacking: Options.AllowPacking ?? Profile.SupportsRequestPacking,
|
||||
ConnectionSize: Options.ConnectionSize ?? Profile.DefaultConnectionSize);
|
||||
|
||||
public void DisposeHandles()
|
||||
{
|
||||
foreach (var r in Runtimes.Values) r.Dispose();
|
||||
|
||||
@@ -51,7 +51,9 @@ public static class AbCipDriverFactoryExtensions
|
||||
$"AB CIP config for '{driverInstanceId}' has a device missing HostAddress"),
|
||||
PlcFamily: ParseEnum<AbCipPlcFamily>(d.PlcFamily, "device", driverInstanceId, "PlcFamily",
|
||||
fallback: AbCipPlcFamily.ControlLogix),
|
||||
DeviceName: d.DeviceName))]
|
||||
DeviceName: d.DeviceName,
|
||||
AllowPacking: d.AllowPacking,
|
||||
ConnectionSize: d.ConnectionSize))]
|
||||
: [],
|
||||
Tags = dto.Tags is { Count: > 0 }
|
||||
? [.. dto.Tags.Select(t => BuildTag(t, driverInstanceId))]
|
||||
@@ -133,6 +135,8 @@ public static class AbCipDriverFactoryExtensions
|
||||
public string? HostAddress { get; init; }
|
||||
public string? PlcFamily { get; init; }
|
||||
public string? DeviceName { get; init; }
|
||||
public bool? AllowPacking { get; init; }
|
||||
public int? ConnectionSize { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class AbCipTagDto
|
||||
|
||||
@@ -18,7 +18,11 @@ public sealed class AbCipDriverOptions
|
||||
/// </summary>
|
||||
public IReadOnlyList<AbCipDeviceOptions> Devices { get; init; } = [];
|
||||
|
||||
/// <summary>Pre-declared tag map across all devices — AB discovery lands in PR 5.</summary>
|
||||
/// <summary>
|
||||
/// Pre-declared tag map across all devices. Pre-declared tags always emit during
|
||||
/// discovery; opt in to controller-side discovery via
|
||||
/// <see cref="EnableControllerBrowse"/>.
|
||||
/// </summary>
|
||||
public IReadOnlyList<AbCipTagDefinition> Tags { get; init; } = [];
|
||||
|
||||
/// <summary>Per-device probe settings. Falls back to defaults when omitted.</summary>
|
||||
@@ -78,13 +82,28 @@ public sealed class AbCipDriverOptions
|
||||
/// initialization rather than silently connecting to nothing.
|
||||
/// </summary>
|
||||
/// <param name="HostAddress">Canonical <c>ab://gateway[:port]/cip-path</c> string.</param>
|
||||
/// <param name="PlcFamily">Which per-family profile to apply. Determines ConnectionSize,
|
||||
/// request-packing support, unconnected-only hint, and other quirks.</param>
|
||||
/// <param name="PlcFamily">Which per-family profile to apply. Determines the family
|
||||
/// <c>AllowPacking</c> default, <c>ConnectionSize</c> default, unconnected-only hint, and
|
||||
/// other quirks; per-device overrides via <see cref="AllowPacking"/> and
|
||||
/// <see cref="ConnectionSize"/> take precedence when set.</param>
|
||||
/// <param name="DeviceName">Optional display label for Admin UI. Falls back to <see cref="HostAddress"/>.</param>
|
||||
/// <param name="AllowPacking">Driver.AbCip-013 — per-device override for CIP request-packing
|
||||
/// (firmware 20+). <c>null</c> (the default) inherits the family profile's
|
||||
/// <c>SupportsRequestPacking</c>; set explicitly to opt a single device in or out without
|
||||
/// touching every other device on the same family.</param>
|
||||
/// <param name="ConnectionSize">Driver.AbCip-013 — per-device override for the Forward Open
|
||||
/// ConnectionSize (Large Forward Open packet size in bytes). <c>null</c> inherits the family
|
||||
/// profile's <c>DefaultConnectionSize</c>. Honoured by the driver layer; the underlying
|
||||
/// libplctag 1.5.2 wrapper has no direct <c>ConnectionSize</c> property, so the value is
|
||||
/// plumbed through <see cref="AbCipTagCreateParams"/> for forward-compat with future wrapper
|
||||
/// versions or a custom tag-attribute path; current builds use the family profile default at
|
||||
/// the wire layer regardless.</param>
|
||||
public sealed record AbCipDeviceOptions(
|
||||
string HostAddress,
|
||||
AbCipPlcFamily PlcFamily = AbCipPlcFamily.ControlLogix,
|
||||
string? DeviceName = null);
|
||||
string? DeviceName = null,
|
||||
bool? AllowPacking = null,
|
||||
int? ConnectionSize = null);
|
||||
|
||||
/// <summary>
|
||||
/// One AB-backed OPC UA variable. Mirrors the <c>ModbusTagDefinition</c> shape.
|
||||
@@ -149,9 +168,11 @@ public sealed class AbCipProbeOptions
|
||||
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
|
||||
|
||||
/// <summary>
|
||||
/// Tag path used for the probe. If null, the driver attempts to read a default
|
||||
/// system tag (PR 8 wires this up — the choice is family-dependent, e.g.
|
||||
/// <c>@raw_cpu_type</c> on ControlLogix or a user-configured probe tag on Micro800).
|
||||
/// Tag path used for the probe. When <see cref="Enabled"/> is <c>true</c> but this is
|
||||
/// <c>null</c>/blank, the driver logs a warning and runs no probe loops (Driver.AbCip-011);
|
||||
/// <c>GetHostStatuses()</c> will then report every device as <c>Unknown</c>. A family-default
|
||||
/// system-tag fallback (e.g. <c>@raw_cpu_type</c> on ControlLogix) is a deferred follow-up;
|
||||
/// today an operator opting into the probe must supply a tag path explicitly.
|
||||
/// </summary>
|
||||
public string? ProbeTagPath { get; init; }
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
/// attribute consumes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Scope + members + subscripts are captured structurally so PR 6 (UDT support) can walk
|
||||
/// the path against a cached template without re-parsing. <see cref="BitIndex"/> is
|
||||
/// Scope + members + subscripts are captured structurally so UDT support can walk the
|
||||
/// path against a cached template (see <see cref="AbCipTemplateCache"/>) without
|
||||
/// re-parsing. <see cref="BitIndex"/> is
|
||||
/// non-null only when the trailing segment is a decimal integer between 0 and 31 that
|
||||
/// parses as a bit-selector — this is the <c>.N</c> syntax documented in the Logix 5000
|
||||
/// General Instructions Reference §Tags, and it applies only to DINT-typed parents. The
|
||||
|
||||
@@ -9,9 +9,10 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
/// <c>ReinitializeAsync</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Template shape read (CIP Template Object class 0x6C, <c>GetAttributeList</c> +
|
||||
/// <c>Read Template</c>) lands with PR 6. This class ships the cache surface so PR 6 can
|
||||
/// drop the decoder in without reshaping any caller code.
|
||||
/// Templates are decoded by <see cref="CipTemplateObjectDecoder"/> (CIP Template Object
|
||||
/// class 0x6C, <c>GetAttributeList</c> + <c>Read Template</c>); the live reader is
|
||||
/// <see cref="LibplctagTemplateReader"/>, and <see cref="AbCipDriver.FetchUdtShapeAsync"/>
|
||||
/// populates this cache on first fetch.
|
||||
/// </remarks>
|
||||
public sealed class AbCipTemplateCache
|
||||
{
|
||||
@@ -36,8 +37,8 @@ public sealed class AbCipTemplateCache
|
||||
|
||||
/// <summary>
|
||||
/// Decoded shape of one Logix UDT — member list + each member's offset + type. Populated
|
||||
/// by PR 6's Template Object reader. At PR 5 time this is the cache's value type only;
|
||||
/// no reader writes to it yet.
|
||||
/// by <see cref="CipTemplateObjectDecoder.Decode"/> from a Template Object response buffer
|
||||
/// read via <see cref="LibplctagTemplateReader"/>.
|
||||
/// </summary>
|
||||
/// <param name="TypeName">UDT name as reported by the Template Object.</param>
|
||||
/// <param name="TotalSize">Bytes the UDT occupies in a whole-UDT read buffer.</param>
|
||||
|
||||
@@ -3,11 +3,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
/// <summary>
|
||||
/// Swappable scanner that walks a controller's symbol table (via libplctag's
|
||||
/// <c>@tags</c> pseudo-tag or the CIP Symbol Object class 0x6B) and yields the tags it
|
||||
/// finds. Defaults to <see cref="EmptyAbCipTagEnumeratorFactory"/> which returns no
|
||||
/// controller-side tags — the full <c>@tags</c> decoder lands as a follow-up PR once
|
||||
/// libplctag 1.5.2 either gains <c>TagInfoPlcMapper</c> upstream or we ship our own
|
||||
/// <c>IPlcMapper</c> for the Symbol Object byte layout (tracked via follow-up task; PR 5
|
||||
/// ships the abstraction + pre-declared-tag emission).
|
||||
/// finds. Production default is <see cref="LibplctagTagEnumeratorFactory"/> which speaks
|
||||
/// to the live controller; <see cref="EmptyAbCipTagEnumeratorFactory"/> is available for
|
||||
/// tests / strict-config deployments where only pre-declared tags should appear.
|
||||
/// </summary>
|
||||
public interface IAbCipTagEnumerator : IDisposable
|
||||
{
|
||||
@@ -30,7 +28,8 @@ public interface IAbCipTagEnumeratorFactory
|
||||
/// <param name="Name">Logix symbolic name as returned by the Symbol Object.</param>
|
||||
/// <param name="ProgramScope">Program name if the tag is program-scoped; <c>null</c> for controller scope.</param>
|
||||
/// <param name="DataType">Detected data type; <see cref="AbCipDataType.Structure"/> when the tag
|
||||
/// is UDT-typed — the UDT shape lookup + per-member expansion ship with PR 6.</param>
|
||||
/// is UDT-typed — per-member expansion runs against the cached
|
||||
/// <see cref="AbCipUdtShape"/> via <see cref="AbCipTemplateCache"/>.</param>
|
||||
/// <param name="ReadOnly"><c>true</c> when the Symbol Object's External Access attribute forbids writes.</param>
|
||||
/// <param name="IsSystemTag">Hint from the enumerator that this is a system / infrastructure tag;
|
||||
/// the driver applies <see cref="AbCipSystemTagFilter"/> on top so the enumerator is not the
|
||||
@@ -43,9 +42,10 @@ public sealed record AbCipDiscoveredTag(
|
||||
bool IsSystemTag = false);
|
||||
|
||||
/// <summary>
|
||||
/// Default production enumerator — currently returns an empty sequence. The real <c>@tags</c>
|
||||
/// walk lands as a follow-up PR. Documented in <c>driver-specs.md §3</c> as the gap the
|
||||
/// Symbol Object walker closes.
|
||||
/// No-op enumerator returning an empty sequence. Useful for tests + strict-config
|
||||
/// deployments where <see cref="AbCipDriverOptions.EnableControllerBrowse"/> is set but the
|
||||
/// operator wants only pre-declared tags to surface. Production drivers use
|
||||
/// <see cref="LibplctagTagEnumeratorFactory"/> instead.
|
||||
/// </summary>
|
||||
internal sealed class EmptyAbCipTagEnumerator : IAbCipTagEnumerator
|
||||
{
|
||||
|
||||
@@ -65,10 +65,19 @@ public interface IAbCipTagFactory
|
||||
/// <param name="LibplctagPlcAttribute">libplctag <c>plc=...</c> attribute, per family profile.</param>
|
||||
/// <param name="TagName">Logix symbolic tag name as emitted by <see cref="AbCipTagPath.ToLibplctagName"/>.</param>
|
||||
/// <param name="Timeout">libplctag operation timeout (applies to Initialize / Read / Write).</param>
|
||||
/// <param name="AllowPacking">CIP request-packing flag — combines the per-device override (if
|
||||
/// any) with the family profile's <c>SupportsRequestPacking</c>. Forwarded to the libplctag
|
||||
/// <c>Tag.AllowPacking</c> property (Driver.AbCip-013).</param>
|
||||
/// <param name="ConnectionSize">Forward Open ConnectionSize — combines the per-device override
|
||||
/// (if any) with the family profile's <c>DefaultConnectionSize</c>. libplctag 1.5.2 has no
|
||||
/// direct <c>ConnectionSize</c> property; the value is plumbed for forward-compat with future
|
||||
/// wrappers / a custom tag-attribute path (Driver.AbCip-013).</param>
|
||||
public sealed record AbCipTagCreateParams(
|
||||
string Gateway,
|
||||
int Port,
|
||||
string CipPath,
|
||||
string LibplctagPlcAttribute,
|
||||
string TagName,
|
||||
TimeSpan Timeout);
|
||||
TimeSpan Timeout,
|
||||
bool AllowPacking = true,
|
||||
int ConnectionSize = 4002);
|
||||
|
||||
@@ -13,9 +13,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
/// tag name is <c>@tags</c>. The decoder walks the concatenated entries + emits
|
||||
/// <see cref="AbCipDiscoveredTag"/> records matching our driver surface.</para>
|
||||
///
|
||||
/// <para>Task #178 closed the stub gap from PR 5 — <see cref="EmptyAbCipTagEnumerator"/>
|
||||
/// is still available for tests that don't want to touch the native library, but the
|
||||
/// production factory default now wires this implementation in.</para>
|
||||
/// <para><see cref="EmptyAbCipTagEnumerator"/> remains available for tests that don't want
|
||||
/// to touch the native library; the production factory default
|
||||
/// (<see cref="LibplctagTagEnumeratorFactory"/>) wires this implementation in.</para>
|
||||
/// </remarks>
|
||||
internal sealed class LibplctagTagEnumerator : IAbCipTagEnumerator
|
||||
{
|
||||
|
||||
@@ -23,7 +23,15 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
|
||||
Protocol = Protocol.ab_eip,
|
||||
Name = p.TagName,
|
||||
Timeout = p.Timeout,
|
||||
// Driver.AbCip-013 — honour the per-device or family-default AllowPacking knob so
|
||||
// operators can disable CIP request-packing for older firmware or a single device.
|
||||
AllowPacking = p.AllowPacking,
|
||||
};
|
||||
// ConnectionSize is captured on AbCipTagCreateParams for forward-compat (driver-specs.md
|
||||
// exposes it as a per-device option) but libplctag.NET 1.5.2 has no direct Tag property
|
||||
// for it. Until the wrapper exposes one (or we ship a custom tag-attribute path), the
|
||||
// family profile DefaultConnectionSize is what the underlying CIP Forward Open
|
||||
// negotiates with — Driver.AbCip-013.
|
||||
}
|
||||
|
||||
public Task InitializeAsync(CancellationToken cancellationToken) => _tag.InitializeAsync(cancellationToken);
|
||||
@@ -108,7 +116,11 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
|
||||
_tag.SetInt32(0, Convert.ToInt32(value));
|
||||
break;
|
||||
case AbCipDataType.Structure:
|
||||
throw new NotSupportedException("Whole-UDT writes land in PR 6.");
|
||||
// Whole-UDT writes are not implemented — operators address individual UDT
|
||||
// members via dotted tag paths, which route per-member through the atomic
|
||||
// encode cases above. A whole-UDT writer is a deferred follow-up.
|
||||
throw new NotSupportedException(
|
||||
"Whole-UDT writes are not supported — address individual member paths instead.");
|
||||
default:
|
||||
throw new NotSupportedException($"AbCipDataType {type} not writable.");
|
||||
}
|
||||
|
||||
@@ -8,6 +8,19 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
/// internally via a normal read call, + returns the raw byte buffer so
|
||||
/// <see cref="CipTemplateObjectDecoder"/> can decode it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Driver.AbCip-012 — by design each <c>FetchUdtShapeAsync</c> call creates one fresh
|
||||
/// <see cref="Tag"/>, pays one CIP connection handshake, reads, and disposes. Per-type
|
||||
/// connect cost is accepted because (a) template reads are a low-frequency discovery path
|
||||
/// (one-shot per UDT type, then the decoded shape is cached in
|
||||
/// <see cref="AbCipTemplateCache"/>), (b) libplctag pools its underlying CIP connections per
|
||||
/// gateway+path so the underlying TCP/EIP session is reused even when individual
|
||||
/// <see cref="Tag"/> instances are torn down, and (c) pooling at the wrapper layer here would
|
||||
/// buy a single Forward Open per device per discovery run — small relative to the rest of a
|
||||
/// bulk-tag-walk discovery. If telemetry ever shows discovery latency dominated by
|
||||
/// template-read connects, revisit by holding one <c>@udt</c>-capable <see cref="Tag"/> per
|
||||
/// device for the duration of a discovery run.
|
||||
/// </remarks>
|
||||
internal sealed class LibplctagTemplateReader : IAbCipTemplateReader
|
||||
{
|
||||
private Tag? _tag;
|
||||
|
||||
@@ -8,7 +8,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.PlcFamilies;
|
||||
/// <remarks>
|
||||
/// Mirrors the shape of the Modbus driver's per-family profiles (DL205, Siemens S7,
|
||||
/// Mitsubishi MELSEC). ControlLogix is the baseline; each subsequent family is a delta.
|
||||
/// Family-specific wire tests ship in PRs 9–12.
|
||||
/// Family-specific behaviour (ControlLogix / CompactLogix / Micro800 / GuardLogix) is
|
||||
/// covered by the unit tests in <c>AbCipPlcFamilyTests</c>.
|
||||
/// </remarks>
|
||||
public sealed record AbCipPlcFamilyProfile(
|
||||
string LibplctagPlcAttribute,
|
||||
|
||||
Reference in New Issue
Block a user