Files
lmxopcua/src/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common/DriverCommandBase.cs
Joseph Doherty 5dac2e9375 Task #249 — Driver test-client CLIs: shared lib + Modbus CLI first
Mirrors the v1 otopcua-cli value prop (ad-hoc shell-level PLC validation) for
the Modbus-TCP driver, and lays down the shared scaffolding that AB CIP, AB
Legacy, S7, and TwinCAT CLIs will build on.

New projects:
  - src/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common/ — DriverCommandBase (verbose
    flag + Serilog config) + SnapshotFormatter (single-tag + table +
    write-result renders with invariant-culture value formatting + OPC UA
    status-code shortnames + UTC-normalised timestamps).
  - src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/ — otopcua-modbus-cli executable.
    Commands: probe, read, write, subscribe. ModbusCommandBase carries the
    host/port/unit-id flags + builds ModbusDriverOptions with Probe.Enabled
    =false (CLI runs are one-shot; driver-internal keep-alive would race).

Commands + coverage:
  - probe              single FC03 + GetHealth() + pretty-print
  - read               region × address × type synth into one driver tag
  - write              same shape + --value parsed per --type
  - subscribe          polled-subscription stream until Ctrl+C

Tests (38 total):
  - 16 SnapshotFormatterTests covering: status-code shortnames, unknown
    codes fall back to hex, null value + timestamp placeholders, bool
    lowercase, float invariant culture, string quoting, write-result shape,
    aligned table columns, mismatched-length rejection, UTC normalisation.
  - 22 Modbus CLI tests:
      · ReadCommandTests.SynthesiseTagName (5 theory cases)
      · WriteCommandParseValueTests (17 cases: bool aliases, unknown rejected,
        Int16 bounds, UInt16/Bcd16 type, Float32/64 invariant culture,
        String passthrough, BitInRegister, Int32 MinValue, non-numeric reject)

Wiring:
  - ZB.MOM.WW.OtOpcUa.slnx grew 4 entries (2 src + 2 tests).
  - docs/Driver.Modbus.Cli.md — operator-facing runbook with examples per
    command + output format + typical workflows.

Regression: full-solution build clean; shared-lib tests 16/0, Modbus CLI tests
22/0.

Next up: repeat the pattern for AB CIP (shares ~40% more with Modbus via
libplctag), then AB Legacy, S7, TwinCAT. The shared base stays as-is unless
one of those exposes a gap the Modbus-first pass missed.

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

61 lines
2.6 KiB
C#

using CliFx;
using CliFx.Attributes;
using CliFx.Infrastructure;
using Serilog;
namespace ZB.MOM.WW.OtOpcUa.Driver.Cli.Common;
/// <summary>
/// Shared base for every driver test-client command (Modbus / AB CIP / AB Legacy / S7 /
/// TwinCAT). Carries the options that are meaningful regardless of protocol — verbose
/// logging + the standard timeout — plus helpers every command implementation wants:
/// Serilog configuration + cancellation-token capture.
/// </summary>
/// <remarks>
/// <para>
/// Each driver CLI sub-classes this with its own protocol-specific base (e.g.
/// <c>ModbusCommandBase</c>) that adds host/port/unit-id + a <c>BuildDriver()</c>
/// factory. That second layer is the point where the driver's <c>{Driver}DriverOptions</c>
/// type plugs in; keeping it out of this common base lets each driver CLI stay a thin
/// executable with no dependency on the other drivers' projects.
/// </para>
/// <para>
/// Why a shared base at all — without this every CLI re-authored the same ~40 lines
/// of Serilog wiring + cancel-token plumbing + verbose flag.
/// </para>
/// </remarks>
public abstract class DriverCommandBase : ICommand
{
/// <summary>
/// Enable Serilog debug-level output. Leave off for clean one-line-per-call output;
/// switch on when diagnosing a connect / PDU-framing / retry problem.
/// </summary>
[CommandOption("verbose", Description = "Enable verbose/debug Serilog output")]
public bool Verbose { get; init; }
/// <summary>
/// Request-level timeout used by the driver's <c>Initialize</c> / <c>Read</c> /
/// <c>Write</c> / probe calls. Defaults per-protocol (Modbus: 2s, AB: 5s, S7: 5s,
/// TwinCAT: 5s) — each driver CLI overrides this property with the appropriate
/// <c>[CommandOption]</c> default.
/// </summary>
public abstract TimeSpan Timeout { get; init; }
public abstract ValueTask ExecuteAsync(IConsole console);
/// <summary>
/// Configures the process-global Serilog logger. Commands call this at the top of
/// <see cref="ExecuteAsync"/> so driver-internal <c>Log.Logger</c> writes land on the
/// same sink as the CLI's operator-facing output.
/// </summary>
protected void ConfigureLogging()
{
var config = new LoggerConfiguration();
if (Verbose)
config.MinimumLevel.Debug().WriteTo.Console();
else
config.MinimumLevel.Warning().WriteTo.Console();
Log.Logger = config.CreateLogger();
}
}