Auto: abcip-3.1 — configurable CIP connection size per device

Closes #235
This commit is contained in:
Joseph Doherty
2026-04-25 22:39:05 -04:00
parent 7cbddd4b4a
commit f6c26db609
11 changed files with 683 additions and 10 deletions

View File

@@ -83,7 +83,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
CipPath: device.ParsedAddress.CipPath,
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
TagName: $"@udt/{templateInstanceId}",
Timeout: _options.Timeout);
Timeout: _options.Timeout,
ConnectionSize: device.ConnectionSize);
try
{
@@ -121,6 +122,31 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
?? throw new InvalidOperationException(
$"AbCip device has invalid HostAddress '{device.HostAddress}' — expected 'ab://gateway[:port]/cip-path'.");
var profile = AbCipPlcFamilyProfile.ForFamily(device.PlcFamily);
// PR abcip-3.1 — validate the optional ConnectionSize override before stamping
// the device. The Kepware-supported range [500..4002] is what the libplctag
// ControlLogix driver supports; anything outside that fails the Forward Open at
// runtime, so we reject it loudly at config time instead.
if (device.ConnectionSize is int explicitSize)
{
if (explicitSize < AbCipConnectionSize.Min || explicitSize > AbCipConnectionSize.Max)
throw new InvalidOperationException(
$"AbCip device '{device.HostAddress}' has ConnectionSize {explicitSize} outside the supported range " +
$"[{AbCipConnectionSize.Min}..{AbCipConnectionSize.Max}].");
// Legacy-firmware warning: families whose profile default is 504 (CompactLogix
// narrow cap, also where v19-and-earlier ControlLogix lives) can't actually
// raise their CIP buffer above 511 bytes — the controller rejects the Forward
// Open. Ship the override anyway so newer firmware can use it, but flag the
// mismatch so operators see it in the warning sink.
if (explicitSize > AbCipConnectionSize.LegacyFirmwareCap
&& profile.DefaultConnectionSize <= AbCipConnectionSize.LegacyFirmwareCap)
{
_options.OnWarning?.Invoke(
$"AbCip device '{device.HostAddress}' family '{device.PlcFamily}' uses a narrow-buffer profile " +
$"(default ConnectionSize {profile.DefaultConnectionSize}); the configured ConnectionSize {explicitSize} " +
$"exceeds the {AbCipConnectionSize.LegacyFirmwareCap}-byte legacy-firmware cap and will fail the " +
"Forward Open on v19-and-earlier ControlLogix or 5069-L1/L2/L3 CompactLogix firmware.");
}
}
_devices[device.HostAddress] = new DeviceState(addr, device, profile);
}
// Pre-declared tags first; L5K imports fill in only the names not already covered
@@ -356,7 +382,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
CipPath: state.ParsedAddress.CipPath,
LibplctagPlcAttribute: state.Profile.LibplctagPlcAttribute,
TagName: _options.Probe.ProbeTagPath!,
Timeout: _options.Probe.Timeout);
Timeout: _options.Probe.Timeout,
ConnectionSize: state.ConnectionSize);
IAbCipTagRuntime? probeRuntime = null;
while (!ct.IsCancellationRequested)
@@ -533,7 +560,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
CipPath: device.ParsedAddress.CipPath,
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
TagName: parsedPath.ToLibplctagName(),
Timeout: _options.Timeout);
Timeout: _options.Timeout,
ConnectionSize: device.ConnectionSize);
var plan = AbCipArrayReadPlanner.TryBuild(def, parsedPath, baseParams);
if (plan is null)
@@ -892,7 +920,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
CipPath: device.ParsedAddress.CipPath,
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
TagName: parentTagName,
Timeout: _options.Timeout));
Timeout: _options.Timeout,
ConnectionSize: device.ConnectionSize));
try
{
await runtime.InitializeAsync(ct).ConfigureAwait(false);
@@ -927,7 +956,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
TagName: parsed.ToLibplctagName(),
Timeout: _options.Timeout,
StringMaxCapacity: def.DataType == AbCipDataType.String ? def.StringLength : null));
StringMaxCapacity: def.DataType == AbCipDataType.String ? def.StringLength : null,
ConnectionSize: device.ConnectionSize));
try
{
await runtime.InitializeAsync(ct).ConfigureAwait(false);
@@ -1081,7 +1111,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
CipPath: state.ParsedAddress.CipPath,
LibplctagPlcAttribute: state.Profile.LibplctagPlcAttribute,
TagName: "@tags",
Timeout: _options.Timeout);
Timeout: _options.Timeout,
ConnectionSize: state.ConnectionSize);
IAddressSpaceBuilder? discoveredFolder = null;
await foreach (var discovered in enumerator.EnumerateAsync(deviceParams, cancellationToken)
@@ -1152,6 +1183,16 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
public AbCipDeviceOptions Options { get; } = options;
public AbCipPlcFamilyProfile Profile { get; } = profile;
/// <summary>
/// PR abcip-3.1 — effective CIP connection size for this device. Per-device
/// <see cref="AbCipDeviceOptions.ConnectionSize"/> override wins; otherwise the
/// family profile's <see cref="AbCipPlcFamilyProfile.DefaultConnectionSize"/>
/// (4002 / 504 / 488 depending on family). Threaded through every
/// <see cref="AbCipTagCreateParams"/> the driver builds so libplctag receives a
/// consistent buffer-size hint across read / write / probe / discovery handles.
/// </summary>
public int ConnectionSize { get; } = options.ConnectionSize ?? profile.DefaultConnectionSize;
public object ProbeLock { get; } = new();
public HostState HostState { get; set; } = HostState.Unknown;
public DateTime HostStateChangedUtc { get; set; } = DateTime.UtcNow;