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.S7.Cli.Commands; /// /// Probes an S7 endpoint: connects via S7.Net, reads one merker word, prints health. /// If the PLC is fresh out of TIA Portal the probe will surface /// BadNotSupported — PUT/GET communication has to be enabled in the hardware /// config for any S7-1200/1500 for the driver to get past the handshake. /// [Command("probe", Description = "Verify the S7 endpoint is reachable and a sample read succeeds.")] public sealed class ProbeCommand : S7CommandBase { /// Gets or sets the S7 address to probe. [CommandOption("address", 'a', Description = "Probe address (default MW0 — merker word 0). DB1.DBW0 if your PLC project " + "reserves a fingerprint DB.")] public string Address { get; init; } = "MW0"; /// Gets or sets the data type of the probe address. [CommandOption("type", Description = "Probe data type (default Int16).")] public S7DataType DataType { get; init; } = S7DataType.Int16; /// public override async ValueTask ExecuteAsync(IConsole console) { ConfigureLogging(); ValidateEndpoint(); var ct = console.RegisterCancellationHandler(); var probeTag = new S7TagDefinition( Name: "__probe", Address: Address, DataType: DataType, Writable: false); var options = BuildOptions([probeTag]); // `await using` is the sole disposal mechanism — S7Driver.DisposeAsync // already invokes ShutdownAsync, so the previous explicit ShutdownAsync(CancellationToken.None) // call in a finally block ran shutdown twice. The await-using on the next line is enough. await using var driver = new S7Driver(options, DriverInstanceId); // Wrap the entire probe sequence so that a refused/unreachable TCP // connect still prints the structured Host/CPU/Health lines instead of crashing with a // full .NET stack trace. InitializeAsync sets health to Faulted with the exception // message before re-throwing, so GetHealth() always has something to report. 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 `Health: Healthy` over a Bad snapshot // line (mirrors Modbus.Cli's ComputeVerdict). var verdict = ComputeVerdict(health.State, snapshot[0].StatusCode); await console.Output.WriteLineAsync($"Host: {Host}:{Port}"); await console.Output.WriteLineAsync($"CPU: {CpuType} rack={Rack} slot={Slot}"); await console.Output.WriteLineAsync($"Verdict: {verdict}"); await console.Output.WriteLineAsync($"Health: {health.State}"); if (health.LastError is { } err) await console.Output.WriteLineAsync($"Last error: {err}"); await console.Output.WriteLineAsync(); await console.Output.WriteLineAsync(SnapshotFormatter.Format(Address, snapshot[0])); } catch (OperationCanceledException) when (ct.IsCancellationRequested) { // Ctrl+C — re-throw so CliFx exits cleanly. The when filter ensures a // driver-internal timeout (different CancellationToken) is not mis-classified // as user cancellation and falls through to the structured-report catch below. throw; } catch { // Connect / read failure — print what the driver knows so far. var health = driver.GetHealth(); await console.Output.WriteLineAsync($"Host: {Host}:{Port}"); await console.Output.WriteLineAsync($"CPU: {CpuType} rack={Rack} slot={Slot}"); await console.Output.WriteLineAsync($"Health: {health.State}"); if (health.LastError is { } err) await console.Output.WriteLineAsync($"Last error: {err}"); } } /// /// Combine the driver-side with the probe snapshot's OPC UA /// StatusCode into a single headline verdict: FAIL when the driver is not /// Healthy or the snapshot is Bad, DEGRADED when Healthy but Uncertain, else OK. /// /// 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) { // Top 2 bits encode the quality class: 00 Good, 01 Uncertain, 10/11 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)})"; } }