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:
Joseph Doherty
2026-10-07 20:49:33 -04:00
parent 17e1170e61
commit ed0881d672
29 changed files with 653 additions and 131 deletions
@@ -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 },
};