Files
lmxopcua/src/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/S7CommandBase.cs
Joseph Doherty 4dc685a365 Task #251 — S7 + TwinCAT test-client CLIs (driver CLI suite complete)
Final two of the five driver test clients. Pattern carried forward from
#249 (Modbus) + #250 (AB CIP, AB Legacy) — each CLI inherits Driver.Cli.Common
for DriverCommandBase + SnapshotFormatter and adds a protocol-specific
CommandBase + 4 commands (probe / read / write / subscribe).

New projects:
  - src/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/ — otopcua-s7-cli.
    S7CommandBase carries host/port/cpu/rack/slot/timeout. Handles all S7
    atomic types (Bool, Byte, Int16..UInt64, Float32/64, String, DateTime).
    DateTime parses via RoundtripKind so "2026-04-21T12:34:56Z" works.
  - src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/ — otopcua-twincat-cli.
    TwinCATCommandBase carries ams-net-id + ams-port + --poll-only toggle
    (flips UseNativeNotifications=false). Covers the full IEC 61131-3
    atomic set: Bool, SInt/USInt, Int/UInt, DInt/UDInt, LInt/ULInt, Real,
    LReal, String, WString, Time/Date/DateTime/TimeOfDay. Structure writes
    refused as out-of-scope (same as AB CIP). IEC time/date variants marshal
    as UDINT on the wire per IEC spec. Subscribe banner announces "ADS
    notification" vs "polling" so the mechanism is obvious in bug reports.

Tests (49 new, 122 cumulative driver-CLI):
  - S7: 22 tests. Every S7DataType has a happy-path + bounds case. DateTime
    round-trips an ISO-8601 string. Tag-name synthesis round-trips every
    S7 address form (DB / M / I / Q, bit/word/dword, strings).
  - TwinCAT: 27 tests. Full IEC type matrix including WString UTF-8 pass-
    through + the four IEC time/date variants landing on UDINT. Structure
    rejection case. Tag-name synthesis for Program scope, GVL scope, nested
    UDT members, and array elements.

Docs:
  - docs/Driver.S7.Cli.md — address grammar cheat sheet + the PUT/GET-must-
    be-enabled gotcha every S7-1200/1500 operator hits.
  - docs/Driver.TwinCAT.Cli.md — AMS router prerequisite (XAR / standalone
    Router NuGet / remote AMS route) + per-command examples.

Wiring:
  - ZB.MOM.WW.OtOpcUa.slnx grew 4 entries (2 src + 2 tests).

Full-solution build clean. Both --help outputs verified end-to-end.

Driver CLI suite complete: 5 CLIs (otopcua-{modbus,abcip,ablegacy,s7,twincat}-cli)
sharing a common base + formatter. 122 CLI tests cumulative. Every driver family
shipped in v2 now has a shell-level ad-hoc validation tool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 08:44:53 -04:00

62 lines
2.3 KiB
C#

using CliFx.Attributes;
using ZB.MOM.WW.OtOpcUa.Driver.Cli.Common;
using S7NetCpuType = global::S7.Net.CpuType;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Cli;
/// <summary>
/// Base for every S7 CLI command. Carries the ISO-on-TCP endpoint options
/// (host / port / CPU type / rack / slot) that S7.Net needs for its handshake +
/// exposes <see cref="BuildOptions"/> so each command can synthesise an
/// <see cref="S7DriverOptions"/> on demand.
/// </summary>
public abstract class S7CommandBase : DriverCommandBase
{
[CommandOption("host", 'h', Description = "PLC IP address or hostname.", IsRequired = true)]
public string Host { get; init; } = default!;
[CommandOption("port", 'p', Description = "ISO-on-TCP port (default 102).")]
public int Port { get; init; } = 102;
[CommandOption("cpu", 'c', Description =
"S7 CPU family: S7200 / S7200Smart / S7300 / S7400 / S71200 / S71500 " +
"(default S71500). Determines the ISO-TSAP slot byte.")]
public S7NetCpuType CpuType { get; init; } = S7NetCpuType.S71500;
[CommandOption("rack", Description = "Rack number (default 0 — single-rack).")]
public short Rack { get; init; } = 0;
[CommandOption("slot", Description =
"CPU slot. S7-300 = 2, S7-400 = 2 or 3, S7-1200 / S7-1500 = 0 (default 0).")]
public short Slot { get; init; } = 0;
[CommandOption("timeout-ms", Description = "Per-operation timeout in ms (default 5000).")]
public int TimeoutMs { get; init; } = 5000;
/// <inheritdoc />
public override TimeSpan Timeout
{
get => TimeSpan.FromMilliseconds(TimeoutMs);
init { /* driven by TimeoutMs */ }
}
/// <summary>
/// Build an <see cref="S7DriverOptions"/> with the endpoint fields this base
/// collected + whatever <paramref name="tags"/> the subclass declares. Probe
/// disabled — CLI runs are one-shot.
/// </summary>
protected S7DriverOptions BuildOptions(IReadOnlyList<S7TagDefinition> tags) => new()
{
Host = Host,
Port = Port,
CpuType = CpuType,
Rack = Rack,
Slot = Slot,
Timeout = Timeout,
Tags = tags,
Probe = new S7ProbeOptions { Enabled = false },
};
protected string DriverInstanceId => $"s7-cli-{Host}:{Port}";
}