Auto: abcip-3.1 — configurable CIP connection size per device
Closes #235
This commit is contained in:
29
src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipConnectionSize.cs
Normal file
29
src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipConnectionSize.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
|
||||
/// <summary>
|
||||
/// PR abcip-3.1 — bounds + magic numbers for the per-device CIP <c>ConnectionSize</c>
|
||||
/// override. Pulled into a single place so config validation, the legacy-firmware warning,
|
||||
/// and the docs stay in sync.
|
||||
/// </summary>
|
||||
public static class AbCipConnectionSize
|
||||
{
|
||||
/// <summary>
|
||||
/// Minimum supported CIP Forward Open buffer size, in bytes. Matches the lower bound of
|
||||
/// Kepware's connection-size slider for ControlLogix drivers + the libplctag native
|
||||
/// floor that still leaves headroom for the CIP MR header.
|
||||
/// </summary>
|
||||
public const int Min = 500;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum supported CIP Forward Open buffer size, in bytes. Matches the upper bound of
|
||||
/// Kepware's slider + the Large Forward Open ceiling on FW20+ ControlLogix.
|
||||
/// </summary>
|
||||
public const int Max = 4002;
|
||||
|
||||
/// <summary>
|
||||
/// Soft cap above which legacy ControlLogix firmware (v19 and earlier) rejects the
|
||||
/// Forward Open. CompactLogix L1/L2/L3 narrow-cap parts (5069-L1/L2/L3) and Micro800
|
||||
/// hard-cap below this too. Used as the threshold for the legacy-firmware warning.
|
||||
/// </summary>
|
||||
public const int LegacyFirmwareCap = 511;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -37,7 +37,8 @@ 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,
|
||||
ConnectionSize: d.ConnectionSize))]
|
||||
: [],
|
||||
Tags = dto.Tags is { Count: > 0 }
|
||||
? [.. dto.Tags.Select(t => BuildTag(t, driverInstanceId))]
|
||||
@@ -119,6 +120,12 @@ public static class AbCipDriverFactoryExtensions
|
||||
public string? HostAddress { get; init; }
|
||||
public string? PlcFamily { get; init; }
|
||||
public string? DeviceName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// PR abcip-3.1 — optional per-device CIP <c>ConnectionSize</c> override. Validated
|
||||
/// against <c>[500..4002]</c> at <see cref="AbCipDriver.InitializeAsync"/>.
|
||||
/// </summary>
|
||||
public int? ConnectionSize { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class AbCipTagDto
|
||||
|
||||
@@ -87,6 +87,14 @@ public sealed class AbCipDriverOptions
|
||||
/// 1 second — matches typical SCADA alarm-refresh conventions.
|
||||
/// </summary>
|
||||
public TimeSpan AlarmPollInterval { get; init; } = TimeSpan.FromSeconds(1);
|
||||
|
||||
/// <summary>
|
||||
/// PR abcip-3.1 — optional sink for non-fatal driver warnings (legacy-firmware
|
||||
/// <c>ConnectionSize</c> mis-match, etc.). Production hosting wires this to Serilog;
|
||||
/// unit tests pin a list-collecting lambda to assert which warnings fired. <c>null</c>
|
||||
/// swallows warnings — convenient for back-compat deployments that don't care.
|
||||
/// </summary>
|
||||
public Action<string>? OnWarning { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -98,10 +106,19 @@ public sealed class AbCipDriverOptions
|
||||
/// <param name="PlcFamily">Which per-family profile to apply. Determines ConnectionSize,
|
||||
/// request-packing support, unconnected-only hint, and other quirks.</param>
|
||||
/// <param name="DeviceName">Optional display label for Admin UI. Falls back to <see cref="HostAddress"/>.</param>
|
||||
/// <param name="ConnectionSize">PR abcip-3.1 — optional override for the family-default
|
||||
/// <see cref="PlcFamilies.AbCipPlcFamilyProfile.DefaultConnectionSize"/>. Threads through to
|
||||
/// libplctag's <c>connection_size</c> attribute on the underlying tag handle so operators can
|
||||
/// dial the CIP Forward Open buffer down for legacy firmware (v19-and-earlier ControlLogix
|
||||
/// caps at 504) or up for high-throughput shops on FW20+. Validated against the Kepware
|
||||
/// supported range [500..4002] at <c>InitializeAsync</c>; out-of-range values fault the
|
||||
/// driver. <c>null</c> uses the family default — back-compat with deployments that haven't
|
||||
/// touched the knob.</param>
|
||||
public sealed record AbCipDeviceOptions(
|
||||
string HostAddress,
|
||||
AbCipPlcFamily PlcFamily = AbCipPlcFamily.ControlLogix,
|
||||
string? DeviceName = null);
|
||||
string? DeviceName = null,
|
||||
int? ConnectionSize = null);
|
||||
|
||||
/// <summary>
|
||||
/// One AB-backed OPC UA variable. Mirrors the <c>ModbusTagDefinition</c> shape.
|
||||
|
||||
@@ -73,6 +73,13 @@ public interface IAbCipTagFactory
|
||||
/// to issue a Rockwell array read covering <c>N</c> consecutive elements starting at the
|
||||
/// subscripted index in <see cref="TagName"/>. Drives PR abcip-1.3 array-slice support;
|
||||
/// <c>null</c> leaves libplctag's default scalar-element behaviour for back-compat.</param>
|
||||
/// <param name="ConnectionSize">PR abcip-3.1 — CIP Forward Open buffer size in bytes. Threads
|
||||
/// through to libplctag's <c>connection_size</c> attribute. The driver always supplies a
|
||||
/// value here — either the per-device <see cref="AbCipDeviceOptions.ConnectionSize"/>
|
||||
/// override or the family profile's <see cref="PlcFamilies.AbCipPlcFamilyProfile.DefaultConnectionSize"/>.
|
||||
/// Bigger packets fit more tags per RTT (higher throughput); smaller packets stay compatible
|
||||
/// with legacy firmware (v19-and-earlier ControlLogix caps at 504, Micro800 hard-caps at
|
||||
/// 488).</param>
|
||||
public sealed record AbCipTagCreateParams(
|
||||
string Gateway,
|
||||
int Port,
|
||||
@@ -81,4 +88,5 @@ public sealed record AbCipTagCreateParams(
|
||||
string TagName,
|
||||
TimeSpan Timeout,
|
||||
int? StringMaxCapacity = null,
|
||||
int? ElementCount = null);
|
||||
int? ElementCount = null,
|
||||
int ConnectionSize = 4002);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Reflection;
|
||||
using libplctag;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
@@ -12,6 +13,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
|
||||
{
|
||||
private readonly Tag _tag;
|
||||
private readonly int _connectionSize;
|
||||
|
||||
public LibplctagTagRuntime(AbCipTagCreateParams p)
|
||||
{
|
||||
@@ -35,12 +37,55 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
|
||||
// to issue one Rockwell array read for a [N..M] slice.
|
||||
if (p.ElementCount is int n && n > 0)
|
||||
_tag.ElementCount = n;
|
||||
_connectionSize = p.ConnectionSize;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _tag.InitializeAsync(cancellationToken).ConfigureAwait(false);
|
||||
// PR abcip-3.1 — propagate the configured CIP connection size to the native libplctag
|
||||
// handle. The 1.5.x C# wrapper does not expose <c>connection_size</c> as a public Tag
|
||||
// property, so we reach into the internal <c>NativeTagWrapper</c>'s
|
||||
// <c>SetIntAttribute</c> (mirroring libplctag's <c>plc_tag_set_int_attribute</c>).
|
||||
// libplctag native parses <c>connection_size</c> at create time, so this best-effort
|
||||
// call lights up automatically when a future wrapper release exposes the attribute or
|
||||
// when libplctag native gains post-create hot-update support — until then it falls back
|
||||
// to the wrapper default. Failures (older / patched wrappers without the internal API)
|
||||
// are intentionally swallowed so the driver keeps initialising.
|
||||
TrySetConnectionSize(_tag, _connectionSize);
|
||||
}
|
||||
|
||||
public Task InitializeAsync(CancellationToken cancellationToken) => _tag.InitializeAsync(cancellationToken);
|
||||
public Task ReadAsync(CancellationToken cancellationToken) => _tag.ReadAsync(cancellationToken);
|
||||
public Task WriteAsync(CancellationToken cancellationToken) => _tag.WriteAsync(cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Best-effort propagation of <c>connection_size</c> to libplctag native. Reflects into
|
||||
/// the wrapper's internal <c>NativeTagWrapper.SetIntAttribute(string, int)</c>; isolated
|
||||
/// in a static helper so the lookup costs run once + the failure path is one line.
|
||||
/// </summary>
|
||||
private static void TrySetConnectionSize(Tag tag, int connectionSize)
|
||||
{
|
||||
try
|
||||
{
|
||||
var wrapperField = typeof(Tag).GetField("_tag", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
var wrapper = wrapperField?.GetValue(tag);
|
||||
if (wrapper is null) return;
|
||||
var setInt = wrapper.GetType().GetMethod(
|
||||
"SetIntAttribute",
|
||||
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
|
||||
binder: null,
|
||||
types: [typeof(string), typeof(int)],
|
||||
modifiers: null);
|
||||
setInt?.Invoke(wrapper, ["connection_size", connectionSize]);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Wrapper internals shifted (newer libplctag.NET) — drop quietly. Either the new
|
||||
// wrapper exposes ConnectionSize directly (our reflection no-ops) or operators must
|
||||
// upgrade to a known-good version per docs/drivers/AbCip-Performance.md.
|
||||
}
|
||||
}
|
||||
|
||||
public int GetStatus() => (int)_tag.GetStatus();
|
||||
|
||||
public object? DecodeValue(AbCipDataType type, int? bitIndex) => DecodeValueAt(type, 0, bitIndex);
|
||||
|
||||
Reference in New Issue
Block a user