826312befe
Clears CommentChecker findings in src/ (3,280 -> 209, the remainder being false positives like PLC-5/IEEE-754/M0.0) by adding missing summaries/params/returns, normalising inheritdoc usage, and removing Gitea/review tracking IDs while keeping the rationale they annotated. Comment-only: comment-stripped sources are identical to the parent commit.
112 lines
5.6 KiB
C#
112 lines
5.6 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// <c>BadNotSupported</c> — PUT/GET communication has to be enabled in the hardware
|
|
/// config for any S7-1200/1500 for the driver to get past the handshake.
|
|
/// </summary>
|
|
[Command("probe", Description = "Verify the S7 endpoint is reachable and a sample read succeeds.")]
|
|
public sealed class ProbeCommand : S7CommandBase
|
|
{
|
|
/// <summary>Gets or sets the S7 address to probe.</summary>
|
|
[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";
|
|
|
|
/// <summary>Gets or sets the data type of the probe address.</summary>
|
|
[CommandOption("type", Description = "Probe data type (default Int16).")]
|
|
public S7DataType DataType { get; init; } = S7DataType.Int16;
|
|
|
|
/// <inheritdoc />
|
|
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}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Combine the driver-side <see cref="DriverState"/> with the probe snapshot's OPC UA
|
|
/// <c>StatusCode</c> into a single headline verdict: <c>FAIL</c> when the driver is not
|
|
/// Healthy or the snapshot is Bad, <c>DEGRADED</c> when Healthy but Uncertain, else <c>OK</c>.
|
|
/// </summary>
|
|
/// <param name="state">The driver's health state.</param>
|
|
/// <param name="statusCode">The OPC UA status code from the probe read.</param>
|
|
/// <returns>A string describing the verdict.</returns>
|
|
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)})";
|
|
}
|
|
}
|