Auto: s7-c5 — pre-flight PUT/GET enablement test

Closes #298
This commit is contained in:
Joseph Doherty
2026-04-26 01:31:48 -04:00
parent 4bc8aa2478
commit 64a11ef285
9 changed files with 625 additions and 3 deletions

View File

@@ -0,0 +1,84 @@
using S7NetErrorCode = global::S7.Net.ErrorCode;
using S7NetPlcException = global::S7.Net.PlcException;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
/// <summary>
/// Thrown by <see cref="S7Driver.InitializeAsync"/> when the post-<c>OpenAsync</c>
/// pre-flight probe receives a response that the driver classifies as
/// "PUT/GET communication disabled on the PLC". Surfaces a typed exception so
/// operators see the configuration-fix instructions immediately at init time
/// instead of waiting for the first per-tag read to fail with
/// <c>BadDeviceFailure</c>. See <c>docs/v2/s7.md</c> "Pre-flight PUT/GET enablement"
/// section for the full rationale.
/// </summary>
public sealed class S7PutGetDisabledException : Exception
{
/// <summary>The probe address that triggered the typed classification (e.g. <c>MW0</c>).</summary>
public string ProbeAddress { get; }
/// <summary>
/// Construct a typed exception. <paramref name="inner"/> is the raw
/// <see cref="S7NetPlcException"/> from S7netplus that
/// <see cref="S7PreflightClassifier.IsPutGetDisabled"/> classified positive.
/// </summary>
public S7PutGetDisabledException(string probeAddress, Exception? inner = null)
: base(BuildMessage(probeAddress), inner)
{
ProbeAddress = probeAddress;
}
internal static string BuildMessage(string probeAddress) =>
"S7 pre-flight probe to '" + probeAddress + "' was rejected by the PLC. " +
"PUT/GET communication is disabled on the PLC. " +
"Enable it in TIA Portal: Device → Properties → Protection & Security → " +
"Connection mechanisms → 'Permit access with PUT/GET communication from " +
"remote partner'. Re-deploy the hardware config and restart the S7 driver. " +
"Alternatively set 'Probe.SkipPreflight = true' to defer the check to runtime " +
"(driver will still surface BadDeviceFailure on every read until PUT/GET is enabled).";
}
/// <summary>
/// Classifies an <see cref="S7NetPlcException"/> coming back from the pre-flight
/// probe read into "PUT/GET disabled" vs "everything else". Pulled out as a static
/// helper so unit tests can drive every branch (matching ErrorCode, non-matching
/// ErrorCode, null exception) without spinning up an S7 server. Mirrors the
/// <c>ModbusPreflight</c> pattern from the Modbus driver.
/// </summary>
public static class S7PreflightClassifier
{
/// <summary>
/// Returns <c>true</c> when the exception's <c>ErrorCode</c> matches the
/// S7.Net surface for "PLC refused the read" — which on hardened S7-1200 /
/// S7-1500 firmware is the wire-level signal for PUT/GET disabled. We match
/// <see cref="S7NetErrorCode.WrongCPU_Type"/> (S7.Net's primary classification
/// when the response framing isn't a valid PDU because the CPU rejected the
/// request) and <see cref="S7NetErrorCode.ReadData"/> (the generic
/// "couldn't read" code S7.Net falls back to when the PLC sends an S7-level
/// error byte instead of a normal response).
/// </summary>
/// <remarks>
/// <para>
/// The S7-1200 / S7-1500 wire response when PUT/GET is disabled is an S7
/// "Function not allowed in current protection level" header byte
/// (0xD605 / 0x8500 family). S7netplus surfaces that as PlcException with
/// <see cref="S7NetErrorCode.ReadData"/> on the response path or
/// <see cref="S7NetErrorCode.WrongCPU_Type"/> when the CPU drops the
/// connection before sending a valid response. Matching both ensures the
/// driver flags the config bug regardless of which path the firmware takes.
/// </para>
/// <para>
/// A non-matching <see cref="S7NetErrorCode"/> (e.g.
/// <see cref="S7NetErrorCode.ConnectionError"/>,
/// <see cref="S7NetErrorCode.IPAddressNotAvailable"/>) means "transport
/// broke" — let the caller rethrow as-is so other failure shapes don't
/// get silently masked as "PUT/GET disabled".
/// </para>
/// </remarks>
public static bool IsPutGetDisabled(S7NetPlcException? exception)
{
if (exception is null) return false;
return exception.ErrorCode is S7NetErrorCode.WrongCPU_Type
or S7NetErrorCode.ReadData;
}
}