feat(ablegacy): Transport option selects native or libplctag across driver, probe and CLI

This commit is contained in:
Joseph Doherty
2026-10-07 16:57:20 -04:00
parent f2377251af
commit 5fd9ff941e
9 changed files with 217 additions and 7 deletions
@@ -25,6 +25,12 @@ public abstract class AbLegacyCommandBase : DriverCommandBase
"Slc500 / MicroLogix / Plc5 / LogixPccc (default Slc500).")]
public AbLegacyPlcFamily PlcType { get; init; } = AbLegacyPlcFamily.Slc500;
/// <summary>Gets the transport stack used to reach the PLC.</summary>
[CommandOption("transport", Description =
"Transport stack: libplctag (default) or native (managed EtherNet/IP + PCCC; PLC-5 N-file reads only, " +
"no status-bit writes, LogixPccc unverified).")]
public AbLegacyTransport Transport { get; init; } = AbLegacyTransport.Libplctag;
/// <summary>Gets the per-operation timeout in milliseconds.</summary>
[CommandOption("timeout-ms", Description = "Per-operation timeout in ms (default 5000).")]
public int TimeoutMs { get; init; } = 5000;
@@ -56,6 +62,7 @@ public abstract class AbLegacyCommandBase : DriverCommandBase
RawTags = [.. tags.Select(t => new RawTagEntry(
t.Name, AbLegacyTagDefinitionFactory.ToTagConfig(t), t.WriteIdempotent, DeviceName: t.DeviceHostAddress))],
Timeout = Timeout,
Transport = Transport,
Probe = new AbLegacyProbeOptions { Enabled = false },
};
@@ -5,7 +5,7 @@ return await new CliApplicationBuilder()
.SetExecutableName("otopcua-ablegacy-cli")
.SetDescription(
"OtOpcUa AB Legacy test-client — ad-hoc probe + PCCC N/F/B/L-file reads/writes + " +
"polled subscriptions against SLC 500 / MicroLogix / PLC-5 devices via libplctag. " +
"polled subscriptions against SLC 500 / MicroLogix / PLC-5 devices via libplctag or the native transport. " +
"Addresses use PCCC convention: N7:0, F8:0, B3:0/3, L19:0, ST17:0.")
.Build()
.RunAsync(args);
@@ -25,6 +25,16 @@ public sealed class AbLegacyDriverOptions
/// </summary>
public IReadOnlyList<RawTagEntry> RawTags { get; init; } = [];
/// <summary>
/// Which transport stack the driver, its Test Connect probe and the CLI use. Defaults to
/// <see cref="AbLegacyTransport.Libplctag"/>.
/// <para>Changing this takes effect when the driver instance is RECREATED, not on
/// <c>ReinitializeAsync</c>: the tag factory is chosen once in the constructor and held across
/// Shutdown+Initialize. A deploy that changes the driver config stops and respawns the instance
/// (<c>DriverSpawnPlanner</c>, #516), so an operator edit does recreate it.</para>
/// </summary>
public AbLegacyTransport Transport { get; init; } = AbLegacyTransport.Libplctag;
/// <summary>Gets or sets the probe (connectivity check) options.</summary>
public AbLegacyProbeOptions Probe { get; init; } = new();
@@ -0,0 +1,17 @@
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
/// <summary>
/// Which PCCC transport stack an AbLegacy driver instance (and its Test Connect probe / CLI run)
/// talks to the controller through. Serialises as a STRING in driver config JSON.
/// </summary>
public enum AbLegacyTransport
{
/// <summary>The libplctag-backed transport. The default.</summary>
Libplctag,
/// <summary>
/// The native managed EtherNet/IP + PCCC transport. Known gaps: PLC-5 supports N-file reads only,
/// writes to status-bit sub-elements are unsupported, and LogixPccc framing is unverified.
/// </summary>
Native,
}
@@ -68,7 +68,11 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
ArgumentNullException.ThrowIfNull(options);
_options = options;
_driverInstanceId = driverInstanceId;
_tagFactory = tagFactory ?? new LibplctagLegacyTagFactory();
// Chosen once here and held across ReinitializeAsync (Shutdown+Initialize): a changed
// Transport in a reinit config is inert; a deploy recreates the instance instead.
_tagFactory = tagFactory ?? (options.Transport == AbLegacyTransport.Native
? new NativeAbLegacyTagFactory()
: new LibplctagLegacyTagFactory());
_logger = logger ?? NullLogger<AbLegacyDriver>.Instance;
_resolver = new EquipmentTagRefResolver<AbLegacyTagDefinition>(
r => _tagsByRawPath.TryGetValue(r, out var t) ? t : null);
@@ -85,6 +89,9 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
new KeyValuePair<string, object?>(OtOpcUaTelemetry.PollOverrunDriverInstanceTag, driverInstanceId)));
}
/// <summary>The tag factory chosen at construction; exposed so tests can pin transport selection.</summary>
internal IAbLegacyTagFactory TagFactory => _tagFactory;
/// <summary>Upper bound on the poll-loop failure backoff — adopts the S7-proven 30 s cap fleet-wide (05/STAB-8).</summary>
private static readonly TimeSpan PollBackoffCap = TimeSpan.FromSeconds(30);
@@ -97,6 +97,8 @@ public static class AbLegacyDriverFactoryExtensions
ProbeAddress = dto.Probe?.ProbeAddress ?? "S:0",
},
Timeout = PositiveTimeoutOrDefault(dto.TimeoutMs, 2_000),
Transport = ParseEnum<AbLegacyTransport>(dto.Transport, driverInstanceId, "Transport",
fallback: AbLegacyTransport.Libplctag),
};
return options;
@@ -146,6 +148,11 @@ public static class AbLegacyDriverFactoryExtensions
/// </summary>
public int? TimeoutMs { get; init; }
/// <summary>
/// Gets or sets the transport (<c>Libplctag</c> or <c>Native</c>, case-insensitive); absent means Libplctag.
/// </summary>
public string? Transport { get; init; }
/// <summary>
/// Gets or sets the list of devices to connect to.
/// </summary>
@@ -30,12 +30,12 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
/// </summary>
public sealed class AbLegacyDriverProbe : IDriverProbe
{
private readonly IAbLegacyTagFactory _tagFactory;
private readonly IAbLegacyTagFactory? _tagFactory;
/// <summary>Initializes a new instance of the <see cref="AbLegacyDriverProbe"/> class.</summary>
/// <param name="tagFactory">The tag factory used for the PCCC handshake; defaults to the libplctag-backed one.</param>
public AbLegacyDriverProbe(IAbLegacyTagFactory? tagFactory = null) =>
_tagFactory = tagFactory ?? new LibplctagLegacyTagFactory();
/// <param name="tagFactory">The tag factory used for the PCCC handshake; when null the config's
/// <c>Transport</c> selects the native or libplctag-backed one.</param>
public AbLegacyDriverProbe(IAbLegacyTagFactory? tagFactory = null) => _tagFactory = tagFactory;
private static readonly JsonSerializerOptions _opts = new()
{
@@ -106,7 +106,10 @@ public sealed class AbLegacyDriverProbe : IDriverProbe
TagName: tagName,
Timeout: timeout);
var rt = _tagFactory.Create(p);
var factory = _tagFactory ?? (opts.Transport == AbLegacyTransport.Native
? new NativeAbLegacyTagFactory()
: new LibplctagLegacyTagFactory());
var rt = factory.Create(p);
try
{
await rt.InitializeAsync(cts.Token);