using CliFx.Attributes; using CliFx.Infrastructure; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.Cli.Common; namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.Commands; /// /// Probes a Modbus-TCP endpoint: opens a socket via 's /// InitializeAsync, issues a single FC03 at the configured probe address, and /// prints the driver's GetHealth(). Fastest way to answer "is the PLC up + talking /// Modbus on this host:port?". /// [Command("probe", Description = "Verify the Modbus-TCP endpoint is reachable and speaks Modbus.")] public sealed class ProbeCommand : ModbusCommandBase { /// Gets the holding-register address to use for the probe read. [CommandOption("probe-address", Description = "Holding-register address used as the cheap-read probe (default 0). Some PLCs lock " + "register 0 — set this to a known-good address on your device.")] public ushort ProbeAddress { get; init; } /// public override async ValueTask ExecuteAsync(IConsole console) { ConfigureLogging(); ValidateEndpoint(); var ct = console.RegisterCancellationHandler(); // Build with one probe tag + Probe.Enabled=false so InitializeAsync connects the // transport, we issue a single read to verify the device responds, then shut down. var probeTag = new ModbusTagDefinition( Name: "__probe", Region: ModbusRegion.HoldingRegisters, Address: ProbeAddress, DataType: ModbusDataType.UInt16); var options = BuildOptions([probeTag]); await using var driver = new ModbusDriver(options, DriverInstanceId); try { await driver.InitializeAsync("{}", ct); var snapshot = await driver.ReadAsync(["__probe"], ct); var health = driver.GetHealth(); // Derive the headline verdict from BOTH the driver state // AND the probe-read StatusCode so the operator never sees the previous // contradictory pair (`Health: Healthy` over a Bad snapshot line). The bare // driver state is still printed below for diagnostics, but the verdict is what // the operator scans first. var verdict = ComputeVerdict(health.State, snapshot[0].StatusCode); await console.Output.WriteLineAsync($"Host: {Host}:{Port} (unit {UnitId})"); await console.Output.WriteLineAsync($"Verdict: {verdict}"); await console.Output.WriteLineAsync($"Driver state: {health.State}"); if (health.LastError is { } err) await console.Output.WriteLineAsync($"Last error: {err}"); await console.Output.WriteLineAsync(); await console.Output.WriteLineAsync( SnapshotFormatter.Format($"HR[{ProbeAddress}]", snapshot[0])); } catch (OperationCanceledException) when (ct.IsCancellationRequested) { // Ctrl+C during InitializeAsync — exit quietly so CliFx // does not render a full stack trace for a user-initiated cancellation. await console.Output.WriteLineAsync("Cancelled."); } finally { await driver.ShutdownAsync(CancellationToken.None); // Flush Serilog before process exit so buffered log lines // emitted during driver shutdown are not lost. Matches Driver.AbCip.Cli pattern. FlushLogging(); } } /// /// Combine the driver-side with the /// probe snapshot's OPC UA StatusCode into a single headline verdict. Order /// of precedence: /// /// FAIL — driver did not reach /// (Faulted / Reconnecting / Unknown) OR the snapshot reports Bad quality. /// DEGRADED — driver Healthy but the snapshot quality is Uncertain. /// OK — driver Healthy and snapshot Good. /// /// /// The driver's health state. /// The OPC UA status code from the probe read. /// A string describing the verdict. public static string ComputeVerdict(DriverState state, uint statusCode) { // OPC UA StatusCode top 2 bits encode the quality class: // 0x00xxxxxx → Good, 0x40xxxxxx → Uncertain, 0x80xxxxxx / 0xC0xxxxxx → Bad. var qualityClass = statusCode & 0xC0000000u; var snapshotGood = qualityClass == 0x00000000u; var snapshotUncertain = qualityClass == 0x40000000u; if (state != DriverState.Healthy || !snapshotGood && !snapshotUncertain) return $"FAIL (driver={state}, probe={SnapshotFormatter.FormatStatus(statusCode)})"; if (snapshotUncertain) return $"DEGRADED (driver={state}, probe={SnapshotFormatter.FormatStatus(statusCode)})"; return $"OK (driver={state}, probe={SnapshotFormatter.FormatStatus(statusCode)})"; } }