fix(ablegacy): final review I3 — default reverts to libplctag; Native is opt-in

On a wire the native PCCC path is proven only for N-file reads/writes and
one F8 read (ab_server). An absent transport key now resolves to Libplctag
for every family; explicit Native still wins and still warns once per
instance on PLC-5/LogixPccc. AdminUI dropdown, CLI help, evidence claims
(PcccFileType, AbLegacy.md wire-evidence table, release note) corrected:
S:0 (default probe) and B/T/C/R/S/L/ST/A/O/I are unverified natively.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-07 21:34:04 -04:00
parent a9fc933e3b
commit b66972b6f5
12 changed files with 166 additions and 130 deletions
@@ -4,16 +4,13 @@ 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>
/// <para>The probe resolves from the config it is GIVEN. The AdminUI merges only the probed device into
/// that config, so in a mixed-family defaulted driver Test Connect on an SLC device can resolve Native
/// while the deployed driver resolves Libplctag (documented in docs/drivers/AbLegacy.md).</para>
/// <see cref="AbLegacyDriverOptions.Transport"/>. The driver constructor, the Test Connect probe and the
/// CLI (through the driver) all resolve through here.
/// <para>An absent key means <see cref="Default"/> = <see cref="AbLegacyTransport.Libplctag"/> for EVERY
/// family (final review I3): on a wire the native PCCC path is proven only for N-file reads and writes
/// and one F8 read, so Native stays opt-in. An explicit value always wins (an operator choice). An
/// explicit Native on a PLC-5 / LogixPccc device is allowed but logs a once-per-instance Warning
/// (<see cref="NativeLimitedFamilies"/>).</para>
/// </summary>
public static class AbLegacyTransportResolver
{
@@ -32,16 +29,13 @@ public static class AbLegacyTransportResolver
return [.. devices.Select(d => d.PlcFamily).Where(IsNativeLimited).Distinct()];
}
/// <summary>Returns the effective transport for a configured value and device set.</summary>
/// <summary>The transport an absent / null <c>transport</c> key selects (all families).</summary>
public const AbLegacyTransport Default = AbLegacyTransport.Libplctag;
/// <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>
/// <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;
}
public static AbLegacyTransport Resolve(AbLegacyTransport? configured) => configured ?? Default;
/// <summary>Returns the effective transport for <paramref name="options"/>.</summary>
/// <param name="options">The driver options.</param>
@@ -49,19 +43,15 @@ public static class AbLegacyTransportResolver
public static AbLegacyTransport Resolve(AbLegacyDriverOptions options)
{
ArgumentNullException.ThrowIfNull(options);
return Resolve(options.Transport, options.Devices);
return Resolve(options.Transport);
}
/// <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>.
/// Operator-facing description of the configured-vs-effective transport for the init log line:
/// <c>Libplctag (default)</c> when the key was absent, <c>Native (configured)</c> when explicit.
/// </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)";
public static string Describe(AbLegacyTransport? configured) =>
configured is null ? $"{Resolve(configured)} (default)" : $"{configured} (configured)";
}