feat(ab): native managed transport is the default for AbCip and AbLegacy; libplctag remains as fallback
Task 22 (spec section 8 P5), with the controller rulings that exclude PLC-5 and LogixPccc. - Transport is nullable on both option records; null (key absent) means "use the default" and is never serialized. Factory DTO parse: absent -> null; a bad value still fails via ParseEnum. - One resolver per driver (AbCipTransportResolver / AbLegacyTransportResolver), used by the driver constructor, the Test Connect probe and (through the driver) the CLI. AbCip: null -> Native. AbLegacy: null -> Native unless any device is Plc5 or LogixPccc, then the whole driver resolves to Libplctag. Explicit values always win; explicit Native with a Plc5/LogixPccc device logs one Warning at init. - Init log shows configured vs effective, e.g. transport=Native (default). - AdminUI forms: "Default (...)" / "Native (managed)" / "libplctag (fallback)". Default omits the key on save (reverses Task 19's explicit re-emit, which would have pinned every edited driver). - CLIs: --transport default|native|libplctag, default "default" via the resolver, so --plc-type Plc5 with no --transport runs libplctag. - Tests pinned to Libplctag where a default-path runtime was created: found by instrumenting the native factories with the current xUnit test name (one unit test, AbLegacyReinitConfigAdoption; plus the two fixture-gated AbCip probe E2E tests whose exact messages were proven on libplctag). TransportMatrix and the differential harness already pass explicit transports. - Switch/resolver/CLI/form tests are falsified by mutating the resolver (default -> Libplctag, exclusion removed, AbLegacy default -> Libplctag): each mutation turns them red. - Replaces the inline role-name literal in AbCipDriverFactoryExtensions' ParseEnum call with a named constant; RoleAuthorityGuardTests was already red on the base commit for that line. - Docs: AbCip.md / AbLegacy.md default + release note; CLAUDE.md AB section. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli;
|
||||
|
||||
/// <summary>
|
||||
/// The CLI's <c>--transport</c> choice. <see cref="Default"/> leaves
|
||||
/// <see cref="AbCipDriverOptions.Transport"/> null so the driver resolves it through
|
||||
/// <see cref="AbCipTransportResolver"/> exactly as an absent <c>transport</c> key in a deployed config does.
|
||||
/// </summary>
|
||||
public enum AbCipCliTransport
|
||||
{
|
||||
/// <summary>Use the driver's default (resolved by <see cref="AbCipTransportResolver"/>).</summary>
|
||||
Default,
|
||||
|
||||
/// <summary>The managed native transport.</summary>
|
||||
Native,
|
||||
|
||||
/// <summary>The libplctag fallback transport.</summary>
|
||||
Libplctag,
|
||||
}
|
||||
|
||||
/// <summary>Maps a <see cref="AbCipCliTransport"/> onto the driver option.</summary>
|
||||
public static class AbCipCliTransportExtensions
|
||||
{
|
||||
/// <summary>Returns the configured transport: null for <see cref="AbCipCliTransport.Default"/>.</summary>
|
||||
/// <param name="choice">The CLI choice.</param>
|
||||
/// <returns>The value for <see cref="AbCipDriverOptions.Transport"/>.</returns>
|
||||
public static AbCipTransport? ToConfigured(this AbCipCliTransport choice) => choice switch
|
||||
{
|
||||
AbCipCliTransport.Native => AbCipTransport.Native,
|
||||
AbCipCliTransport.Libplctag => AbCipTransport.Libplctag,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
@@ -28,8 +28,8 @@ public abstract class AbCipCommandBase : DriverCommandBase
|
||||
|
||||
/// <summary>Gets the transport stack used to reach the PLC.</summary>
|
||||
[CommandOption("transport", Description =
|
||||
"Transport stack: libplctag (default) or native (managed EtherNet/IP + CIP).")]
|
||||
public AbCipTransport Transport { get; init; } = AbCipTransport.Libplctag;
|
||||
"Transport stack: default (= native), native (managed EtherNet/IP + CIP) or libplctag (fallback).")]
|
||||
public AbCipCliTransport Transport { get; init; } = AbCipCliTransport.Default;
|
||||
|
||||
/// <summary>Gets the per-operation timeout in milliseconds.</summary>
|
||||
[CommandOption("timeout-ms", Description = "Per-operation timeout in ms (default 5000).")]
|
||||
@@ -73,7 +73,7 @@ public abstract class AbCipCommandBase : DriverCommandBase
|
||||
WriteIdempotent: t.WriteIdempotent,
|
||||
DeviceName: t.DeviceHostAddress))],
|
||||
Timeout = Timeout,
|
||||
Transport = Transport,
|
||||
Transport = Transport.ToConfigured(),
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
EnableControllerBrowse = false,
|
||||
EnableAlarmProjection = false,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli;
|
||||
|
||||
/// <summary>
|
||||
/// The CLI's <c>--transport</c> choice. <see cref="Default"/> leaves
|
||||
/// <see cref="AbLegacyDriverOptions.Transport"/> null so the driver resolves it through
|
||||
/// <see cref="AbLegacyTransportResolver"/> exactly as an absent <c>transport</c> key in a deployed config does.
|
||||
/// </summary>
|
||||
public enum AbLegacyCliTransport
|
||||
{
|
||||
/// <summary>Use the driver's default (resolved by <see cref="AbLegacyTransportResolver"/>).</summary>
|
||||
Default,
|
||||
|
||||
/// <summary>The managed native transport.</summary>
|
||||
Native,
|
||||
|
||||
/// <summary>The libplctag fallback transport.</summary>
|
||||
Libplctag,
|
||||
}
|
||||
|
||||
/// <summary>Maps a <see cref="AbLegacyCliTransport"/> onto the driver option.</summary>
|
||||
public static class AbLegacyCliTransportExtensions
|
||||
{
|
||||
/// <summary>Returns the configured transport: null for <see cref="AbLegacyCliTransport.Default"/>.</summary>
|
||||
/// <param name="choice">The CLI choice.</param>
|
||||
/// <returns>The value for <see cref="AbLegacyDriverOptions.Transport"/>.</returns>
|
||||
public static AbLegacyTransport? ToConfigured(this AbLegacyCliTransport choice) => choice switch
|
||||
{
|
||||
AbLegacyCliTransport.Native => AbLegacyTransport.Native,
|
||||
AbLegacyCliTransport.Libplctag => AbLegacyTransport.Libplctag,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
@@ -27,9 +27,9 @@ public abstract class AbLegacyCommandBase : DriverCommandBase
|
||||
|
||||
/// <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;
|
||||
"Transport stack: default, native or libplctag. default = native, except libplctag for --plc-type " +
|
||||
"Plc5 / LogixPccc (native there is PLC-5 N-file reads only, no status-bit writes, LogixPccc unverified).")]
|
||||
public AbLegacyCliTransport Transport { get; init; } = AbLegacyCliTransport.Default;
|
||||
|
||||
/// <summary>Gets the per-operation timeout in milliseconds.</summary>
|
||||
[CommandOption("timeout-ms", Description = "Per-operation timeout in ms (default 5000).")]
|
||||
@@ -62,7 +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,
|
||||
Transport = Transport.ToConfigured(),
|
||||
Probe = new AbLegacyProbeOptions { Enabled = false },
|
||||
};
|
||||
|
||||
|
||||
@@ -33,14 +33,20 @@ public sealed class AbCipDriverOptions
|
||||
public IReadOnlyList<RawTagEntry> RawTags { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Which transport stack the driver, its Test Connect probe and the CLI use. Defaults to
|
||||
/// <see cref="AbCipTransport.Libplctag"/>.
|
||||
/// Which transport stack the driver, its Test Connect probe and the CLI use. <c>null</c> (the
|
||||
/// <c>transport</c> key absent) means "use the default", which is <see cref="AbCipTransport.Native"/>;
|
||||
/// an explicit value always wins. The effective transport is resolved in one place,
|
||||
/// <c>AbCipTransportResolver</c>. Set <see cref="AbCipTransport.Libplctag"/> to fall back.
|
||||
/// <para>Changing this takes effect when the driver instance is RECREATED, not on
|
||||
/// <c>ReinitializeAsync</c>: the factories are 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 AbCipTransport Transport { get; init; } = AbCipTransport.Libplctag;
|
||||
/// <remarks>Null is never written: an absent key IS the "use the default" encoding, so a round-trip
|
||||
/// through any serializer (the AdminUI form included) keeps a defaulted driver defaulted rather than
|
||||
/// pinning it to whatever the default happened to be on the day it was saved.</remarks>
|
||||
[System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)]
|
||||
public AbCipTransport? Transport { get; init; }
|
||||
|
||||
/// <summary>Per-device probe settings. Falls back to defaults when omitted.</summary>
|
||||
public AbCipProbeOptions Probe { get; init; } = new();
|
||||
|
||||
@@ -137,9 +137,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
/// <summary>Initializes a new instance of the <see cref="AbCipDriver"/> class.</summary>
|
||||
/// <param name="options">The driver configuration options.</param>
|
||||
/// <param name="driverInstanceId">A unique identifier for this driver instance.</param>
|
||||
/// <param name="tagFactory">Optional factory for creating tag runtimes; uses libplctag default if null.</param>
|
||||
/// <param name="enumeratorFactory">Optional factory for enumerating tags; uses libplctag default if null.</param>
|
||||
/// <param name="templateReaderFactory">Optional factory for reading UDT templates; uses libplctag default if null.</param>
|
||||
/// <param name="tagFactory">Optional factory for creating tag runtimes; when null the resolved transport's (see <see cref="AbCipTransportResolver"/>) is used.</param>
|
||||
/// <param name="enumeratorFactory">Optional factory for enumerating tags; when null the resolved transport's is used.</param>
|
||||
/// <param name="templateReaderFactory">Optional factory for reading UDT templates; when null the resolved transport's is used.</param>
|
||||
/// <param name="logger">Optional logger; uses null logger if not provided.</param>
|
||||
public AbCipDriver(AbCipDriverOptions options, string driverInstanceId,
|
||||
IAbCipTagFactory? tagFactory = null,
|
||||
@@ -152,8 +152,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
_driverInstanceId = driverInstanceId;
|
||||
// Chosen once here and held across ReinitializeAsync (Shutdown+Initialize): a changed
|
||||
// Transport in a reinit config is inert; a deploy recreates the instance instead.
|
||||
_transport = options.Transport;
|
||||
var defaults = AbCipTransportFactories.For(options.Transport);
|
||||
_transport = AbCipTransportResolver.Resolve(options);
|
||||
_transportDescription = AbCipTransportResolver.Describe(options.Transport);
|
||||
var defaults = AbCipTransportFactories.For(_transport);
|
||||
_tagFactory = tagFactory ?? defaults.Tags;
|
||||
_enumeratorFactory = enumeratorFactory ?? defaults.Enumerators;
|
||||
_templateReaderFactory = templateReaderFactory ?? defaults.Templates;
|
||||
@@ -179,10 +180,16 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
/// <summary>The tag factory chosen at construction; exposed so tests can pin transport selection.</summary>
|
||||
internal IAbCipTagFactory TagFactory => _tagFactory;
|
||||
|
||||
/// <summary>The effective transport resolved at construction (see <see cref="AbCipTransportResolver"/>).</summary>
|
||||
internal AbCipTransport EffectiveTransport => _transport;
|
||||
|
||||
// The transport chosen at construction (see the ctor); logged at every init so an operator can tell
|
||||
// from the node log which stack serves this instance.
|
||||
private readonly AbCipTransport _transport;
|
||||
|
||||
// Configured-vs-effective rendering of _transport for the init log, e.g. "Native (default)".
|
||||
private readonly string _transportDescription;
|
||||
|
||||
/// <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);
|
||||
|
||||
@@ -289,7 +296,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
_health = new DriverHealth(DriverState.Initializing, null, null);
|
||||
_logger.LogInformation(
|
||||
"AbCip driver {DriverInstanceId} initializing: transport={Transport} tagFactory={TagFactory}",
|
||||
_driverInstanceId, _transport, _tagFactory.GetType().Name);
|
||||
_driverInstanceId, _transportDescription, _tagFactory.GetType().Name);
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(driverConfigJson))
|
||||
|
||||
@@ -96,8 +96,11 @@ public static class AbCipDriverFactoryExtensions
|
||||
ProbeTagPath = dto.Probe?.ProbeTagPath,
|
||||
},
|
||||
Timeout = PositiveTimeoutOrDefault(dto.TimeoutMs, 2_000),
|
||||
Transport = ParseEnum<AbCipTransport>(dto.Transport, "driver", driverInstanceId, "Transport",
|
||||
fallback: AbCipTransport.Libplctag),
|
||||
// Absent ⇒ null ("use the default", resolved by AbCipTransportResolver); a bad value still
|
||||
// fails exactly as any other bad enum field does.
|
||||
Transport = string.IsNullOrWhiteSpace(dto.Transport)
|
||||
? null
|
||||
: ParseEnum<AbCipTransport>(dto.Transport, DriverConfigOwner, driverInstanceId, "Transport"),
|
||||
EnableControllerBrowse = dto.EnableControllerBrowse ?? false,
|
||||
EnableAlarmProjection = dto.EnableAlarmProjection ?? false,
|
||||
AlarmPollInterval = TimeSpan.FromMilliseconds(dto.AlarmPollIntervalMs ?? 1_000),
|
||||
@@ -120,6 +123,10 @@ public static class AbCipDriverFactoryExtensions
|
||||
private static TimeSpan PositiveTimeoutOrDefault(int? ms, int defaultMs) =>
|
||||
TimeSpan.FromMilliseconds(ms is int v and > 0 ? v : defaultMs);
|
||||
|
||||
// Names the owner of a driver-level enum field in ParseEnum's error text. A named constant rather
|
||||
// than an inline literal: RoleAuthorityGuardTests forbids a bare role-name string literal outside RoleParser.
|
||||
private const string DriverConfigOwner = "(driver config)";
|
||||
|
||||
private static T ParseEnum<T>(string? raw, string? tagName, string driverInstanceId, string field,
|
||||
T? fallback = null) where T : struct, Enum
|
||||
{
|
||||
|
||||
@@ -123,7 +123,7 @@ public sealed class AbCipDriverProbe : IDriverProbe
|
||||
ConnectionSize: firstDevice?.ConnectionSize ?? profile.DefaultConnectionSize,
|
||||
UseConnectedMessaging: profile.SupportsConnectedMessaging);
|
||||
|
||||
var rt = (_tagFactory ?? AbCipTransportFactories.For(opts.Transport).Tags).Create(p);
|
||||
var rt = (_tagFactory ?? AbCipTransportFactories.For(AbCipTransportResolver.Resolve(opts)).Tags).Create(p);
|
||||
try
|
||||
{
|
||||
await rt.InitializeAsync(cts.Token);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
|
||||
/// <summary>
|
||||
/// The ONE place an AbCip driver's effective transport is decided from its configured
|
||||
/// <see cref="AbCipDriverOptions.Transport"/>. The driver constructor, the Test Connect probe and the
|
||||
/// CLI (through the driver) all resolve through here, so they cannot disagree about what an absent
|
||||
/// <c>transport</c> key means.
|
||||
/// </summary>
|
||||
public static class AbCipTransportResolver
|
||||
{
|
||||
/// <summary>The transport an absent / null <c>transport</c> key selects.</summary>
|
||||
public const AbCipTransport Default = AbCipTransport.Native;
|
||||
|
||||
/// <summary>Returns the effective transport: an explicit value always wins; null means <see cref="Default"/>.</summary>
|
||||
/// <param name="configured">The configured transport, or null for "use the default".</param>
|
||||
/// <returns>The transport the driver runs on.</returns>
|
||||
public static AbCipTransport Resolve(AbCipTransport? configured) => configured ?? Default;
|
||||
|
||||
/// <summary>Returns the effective transport for <paramref name="options"/>.</summary>
|
||||
/// <param name="options">The driver options.</param>
|
||||
/// <returns>The transport the driver runs on.</returns>
|
||||
public static AbCipTransport Resolve(AbCipDriverOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
return Resolve(options.Transport);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Operator-facing description of the configured-vs-effective transport for the init log line:
|
||||
/// <c>Native (default)</c> when the key was absent, the bare transport name when it was explicit.
|
||||
/// </summary>
|
||||
/// <param name="configured">The configured transport, or null.</param>
|
||||
/// <returns>A short description.</returns>
|
||||
public static string Describe(AbCipTransport? configured) =>
|
||||
configured is null ? $"{Resolve(configured)} (default)" : $"{configured} (configured)";
|
||||
}
|
||||
@@ -26,14 +26,21 @@ public sealed class AbLegacyDriverOptions
|
||||
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"/>.
|
||||
/// Which transport stack the driver, its Test Connect probe and the CLI use. <c>null</c> (the
|
||||
/// <c>transport</c> key absent) means "use the default": <see cref="AbLegacyTransport.Native"/>, UNLESS
|
||||
/// any configured device is a PLC-5 or LogixPccc (limited native support), in which case the whole
|
||||
/// driver resolves to <see cref="AbLegacyTransport.Libplctag"/>. An explicit value always wins. The
|
||||
/// effective transport is resolved in one place, <c>AbLegacyTransportResolver</c>.
|
||||
/// <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;
|
||||
/// <remarks>Null is never written: an absent key IS the "use the default" encoding, so a round-trip
|
||||
/// through any serializer (the AdminUI form included) keeps a defaulted driver defaulted rather than
|
||||
/// pinning it to whatever the default happened to be on the day it was saved.</remarks>
|
||||
[System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)]
|
||||
public AbLegacyTransport? Transport { get; init; }
|
||||
|
||||
/// <summary>Gets or sets the probe (connectivity check) options.</summary>
|
||||
public AbLegacyProbeOptions Probe { get; init; } = new();
|
||||
|
||||
@@ -70,8 +70,9 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
||||
_driverInstanceId = driverInstanceId;
|
||||
// Chosen once here and held across ReinitializeAsync (Shutdown+Initialize): a changed
|
||||
// Transport in a reinit config is inert; a deploy recreates the instance instead.
|
||||
_transport = options.Transport;
|
||||
_tagFactory = tagFactory ?? (options.Transport == AbLegacyTransport.Native
|
||||
_transport = AbLegacyTransportResolver.Resolve(options);
|
||||
_transportDescription = AbLegacyTransportResolver.Describe(options.Transport, _transport);
|
||||
_tagFactory = tagFactory ?? (_transport == AbLegacyTransport.Native
|
||||
? new NativeAbLegacyTagFactory()
|
||||
: new LibplctagLegacyTagFactory());
|
||||
_logger = logger ?? NullLogger<AbLegacyDriver>.Instance;
|
||||
@@ -93,10 +94,16 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
||||
/// <summary>The tag factory chosen at construction; exposed so tests can pin transport selection.</summary>
|
||||
internal IAbLegacyTagFactory TagFactory => _tagFactory;
|
||||
|
||||
/// <summary>The effective transport resolved at construction (see <see cref="AbLegacyTransportResolver"/>).</summary>
|
||||
internal AbLegacyTransport EffectiveTransport => _transport;
|
||||
|
||||
// The transport chosen at construction (see the ctor); logged at every init so an operator can tell
|
||||
// from the node log which stack serves this instance.
|
||||
private readonly AbLegacyTransport _transport;
|
||||
|
||||
// Configured-vs-effective rendering of _transport for the init log, e.g. "Native (default)".
|
||||
private readonly string _transportDescription;
|
||||
|
||||
/// <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);
|
||||
|
||||
@@ -136,7 +143,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
||||
_health = new DriverHealth(DriverState.Initializing, null, null);
|
||||
_logger.LogInformation(
|
||||
"AbLegacy driver {DriverInstanceId} initializing: transport={Transport} tagFactory={TagFactory}",
|
||||
_driverInstanceId, _transport, _tagFactory.GetType().Name);
|
||||
_driverInstanceId, _transportDescription, _tagFactory.GetType().Name);
|
||||
try
|
||||
{
|
||||
// #516: re-parse the supplied DriverConfig so a config change delivered through the IDriver
|
||||
@@ -145,6 +152,19 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
||||
if (HasConfigBody(driverConfigJson))
|
||||
_options = AbLegacyDriverFactoryExtensions.ParseOptions(_driverInstanceId, driverConfigJson);
|
||||
|
||||
// The resolver only defaults a PLC-5 / LogixPccc driver to libplctag; an operator may still pick
|
||||
// Native explicitly (or a reinit may add such a device to a driver resolved Native). Allowed, but
|
||||
// the limitation is client-visible, so name it once per init.
|
||||
if (_transport == AbLegacyTransport.Native
|
||||
&& AbLegacyTransportResolver.NativeLimitedFamilies(_options.Devices) is { Count: > 0 } limited)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"AbLegacy driver {DriverInstanceId} runs the Native transport against {Families} device(s); native " +
|
||||
"support is limited there (PLC-5: N-file reads only; LogixPccc: framing unverified). Set " +
|
||||
"\"transport\": \"Libplctag\" to use the fallback stack.",
|
||||
_driverInstanceId, string.Join(", ", limited));
|
||||
}
|
||||
|
||||
foreach (var device in _options.Devices)
|
||||
{
|
||||
var addr = AbLegacyHostAddress.TryParse(device.HostAddress)
|
||||
|
||||
@@ -97,8 +97,11 @@ 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),
|
||||
// Absent ⇒ null ("use the default", resolved by AbLegacyTransportResolver against the devices);
|
||||
// a bad value still fails exactly as any other bad enum field does.
|
||||
Transport = string.IsNullOrWhiteSpace(dto.Transport)
|
||||
? null
|
||||
: ParseEnum<AbLegacyTransport>(dto.Transport, driverInstanceId, "Transport"),
|
||||
};
|
||||
|
||||
return options;
|
||||
|
||||
@@ -106,7 +106,7 @@ public sealed class AbLegacyDriverProbe : IDriverProbe
|
||||
TagName: tagName,
|
||||
Timeout: timeout);
|
||||
|
||||
var factory = _tagFactory ?? (opts.Transport == AbLegacyTransport.Native
|
||||
var factory = _tagFactory ?? (AbLegacyTransportResolver.Resolve(opts) == AbLegacyTransport.Native
|
||||
? new NativeAbLegacyTagFactory()
|
||||
: new LibplctagLegacyTagFactory());
|
||||
var rt = factory.Create(p);
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
||||
|
||||
/// <summary>
|
||||
/// The ONE place an AbLegacy driver's effective transport is decided from its configured
|
||||
/// <see cref="AbLegacyDriverOptions.Transport"/> and its devices. The driver constructor, the Test
|
||||
/// Connect probe and the CLI (through the driver) all resolve through here.
|
||||
/// <para>An absent key means <see cref="AbLegacyTransport.Native"/> — UNLESS any configured device is a
|
||||
/// <see cref="AbLegacyPlcFamily.Plc5"/> or <see cref="AbLegacyPlcFamily.LogixPccc"/>, whose native support
|
||||
/// is limited (PLC-5: N-file reads only; LogixPccc: framing unverified). The tag factory is chosen once
|
||||
/// per DRIVER, not per device, so one such device resolves the whole driver to
|
||||
/// <see cref="AbLegacyTransport.Libplctag"/>. An explicit value always wins (an operator choice).</para>
|
||||
/// </summary>
|
||||
public static class AbLegacyTransportResolver
|
||||
{
|
||||
/// <summary>Returns true for a family whose native transport support is limited.</summary>
|
||||
/// <param name="family">The PLC family.</param>
|
||||
/// <returns>True for PLC-5 and LogixPccc.</returns>
|
||||
public static bool IsNativeLimited(AbLegacyPlcFamily family) =>
|
||||
family is AbLegacyPlcFamily.Plc5 or AbLegacyPlcFamily.LogixPccc;
|
||||
|
||||
/// <summary>Returns the distinct native-limited families among <paramref name="devices"/>.</summary>
|
||||
/// <param name="devices">The configured devices.</param>
|
||||
/// <returns>The limited families present, in declaration order.</returns>
|
||||
public static IReadOnlyList<AbLegacyPlcFamily> NativeLimitedFamilies(IEnumerable<AbLegacyDeviceOptions> devices)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(devices);
|
||||
return [.. devices.Select(d => d.PlcFamily).Where(IsNativeLimited).Distinct()];
|
||||
}
|
||||
|
||||
/// <summary>Returns the effective transport for a configured value and device set.</summary>
|
||||
/// <param name="configured">The configured transport, or null for "use the default".</param>
|
||||
/// <param name="devices">The configured devices.</param>
|
||||
/// <returns>The transport the driver runs on.</returns>
|
||||
public static AbLegacyTransport Resolve(AbLegacyTransport? configured, IEnumerable<AbLegacyDeviceOptions> devices)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(devices);
|
||||
if (configured is { } explicitValue) return explicitValue;
|
||||
return devices.Any(d => IsNativeLimited(d.PlcFamily)) ? AbLegacyTransport.Libplctag : AbLegacyTransport.Native;
|
||||
}
|
||||
|
||||
/// <summary>Returns the effective transport for <paramref name="options"/>.</summary>
|
||||
/// <param name="options">The driver options.</param>
|
||||
/// <returns>The transport the driver runs on.</returns>
|
||||
public static AbLegacyTransport Resolve(AbLegacyDriverOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
return Resolve(options.Transport, options.Devices);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Operator-facing description of the configured-vs-effective transport for the init log line, e.g.
|
||||
/// <c>Native (default)</c>, <c>Libplctag (default; PLC-5/LogixPccc device present)</c> or
|
||||
/// <c>Native (configured)</c>.
|
||||
/// </summary>
|
||||
/// <param name="configured">The configured transport, or null.</param>
|
||||
/// <param name="effective">The effective transport.</param>
|
||||
/// <returns>A short description.</returns>
|
||||
public static string Describe(AbLegacyTransport? configured, AbLegacyTransport effective) =>
|
||||
configured is not null ? $"{effective} (configured)"
|
||||
: effective == AbLegacyTransport.Libplctag ? $"{effective} (default; PLC-5/LogixPccc device present)"
|
||||
: $"{effective} (default)";
|
||||
}
|
||||
Reference in New Issue
Block a user