ed0881d672
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
104 lines
5.0 KiB
C#
104 lines
5.0 KiB
C#
using CliFx.Attributes;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Cli.Common;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli;
|
|
|
|
/// <summary>
|
|
/// Base for every AB CIP CLI command. Carries the libplctag endpoint options
|
|
/// (<c>--gateway</c> + <c>--family</c>) and exposes <see cref="BuildOptions"/> so each
|
|
/// command can synthesise an <see cref="AbCipDriverOptions"/> from CLI flags + its own
|
|
/// tag list.
|
|
/// </summary>
|
|
public abstract class AbCipCommandBase : DriverCommandBase
|
|
{
|
|
/// <summary>Gets the canonical AB CIP gateway address.</summary>
|
|
[CommandOption("gateway", 'g', Description =
|
|
"Canonical AB CIP gateway: ab://host[:port]/cip-path. Port defaults to 44818 " +
|
|
"(EtherNet/IP). cip-path is family-specific: ControlLogix / CompactLogix need " +
|
|
"'1,0' to reach slot 0 of the CPU chassis; Micro800 takes an empty path; " +
|
|
"GuardLogix typically '1,0' same as ControlLogix.",
|
|
IsRequired = true)]
|
|
public string Gateway { get; init; } = default!;
|
|
|
|
/// <summary>Gets the PLC family type.</summary>
|
|
[CommandOption("family", 'f', Description =
|
|
"ControlLogix / CompactLogix / Micro800 / GuardLogix (default ControlLogix).")]
|
|
public AbCipPlcFamily Family { get; init; } = AbCipPlcFamily.ControlLogix;
|
|
|
|
/// <summary>Gets the transport stack used to reach the PLC.</summary>
|
|
[CommandOption("transport", Description =
|
|
"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).")]
|
|
public int TimeoutMs { get; init; } = 5000;
|
|
|
|
/// <inheritdoc />
|
|
public override TimeSpan Timeout
|
|
{
|
|
get
|
|
{
|
|
if (TimeoutMs <= 0)
|
|
throw new CliFx.Exceptions.CommandException(
|
|
$"--timeout-ms must be > 0 (got {TimeoutMs}). " +
|
|
"Pick a positive number of milliseconds for the per-operation timeout.");
|
|
return TimeSpan.FromMilliseconds(TimeoutMs);
|
|
}
|
|
init => throw new NotSupportedException(
|
|
$"{nameof(AbCipCommandBase)}.{nameof(Timeout)} is derived from {nameof(TimeoutMs)} " +
|
|
"and cannot be assigned directly. Set TimeoutMs instead.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Build an <see cref="AbCipDriverOptions"/> with the device + tag list a subclass
|
|
/// supplies. Probe + alarm projection are disabled — CLI runs are one-shot; the
|
|
/// probe loop would race the operator's own reads. Each typed tag is serialised to the v3
|
|
/// <see cref="RawTagEntry"/> shape (RawPath = tag name, TagConfig blob via
|
|
/// <see cref="AbCipTagDefinitionFactory.ToTagConfig"/>, DeviceName = the tag's device routing key)
|
|
/// — the same seam the deploy artifact uses.
|
|
/// </summary>
|
|
/// <param name="tags">The list of tag definitions to include in the options.</param>
|
|
/// <returns>The constructed <see cref="AbCipDriverOptions"/>.</returns>
|
|
protected AbCipDriverOptions BuildOptions(IReadOnlyList<AbCipTagDefinition> tags) => new()
|
|
{
|
|
Devices = [new AbCipDeviceOptions(
|
|
HostAddress: Gateway,
|
|
PlcFamily: Family,
|
|
DeviceName: $"cli-{Family}")],
|
|
RawTags = [.. tags.Select(t => new RawTagEntry(
|
|
RawPath: t.Name,
|
|
TagConfig: AbCipTagDefinitionFactory.ToTagConfig(t),
|
|
WriteIdempotent: t.WriteIdempotent,
|
|
DeviceName: t.DeviceHostAddress))],
|
|
Timeout = Timeout,
|
|
Transport = Transport.ToConfigured(),
|
|
Probe = new AbCipProbeOptions { Enabled = false },
|
|
EnableControllerBrowse = false,
|
|
EnableAlarmProjection = false,
|
|
};
|
|
|
|
/// <summary>
|
|
/// Short instance id used in Serilog output so operators running the CLI against
|
|
/// multiple gateways in parallel can distinguish the logs.
|
|
/// </summary>
|
|
protected string DriverInstanceId => $"abcip-cli-{Gateway}";
|
|
|
|
/// <summary>
|
|
/// Guards against <see cref="AbCipDataType.Structure"/> being passed to a command
|
|
/// that does not support UDT layouts. Call at the top of <c>ExecuteAsync</c> for any
|
|
/// command that accepts <c>--type</c> but cannot handle memberless Structure tags.
|
|
/// Throws a <see cref="CliFx.Exceptions.CommandException"/> if <paramref name="type"/>
|
|
/// is <see cref="AbCipDataType.Structure"/>.
|
|
/// </summary>
|
|
/// <param name="type">The data type to validate.</param>
|
|
protected static void RejectStructure(AbCipDataType type)
|
|
{
|
|
if (type == AbCipDataType.Structure)
|
|
throw new CliFx.Exceptions.CommandException(
|
|
"Structure (UDT) reads are out of scope for this command — those need an explicit " +
|
|
"member layout, which belongs in a real driver config.");
|
|
}
|
|
}
|