ee12568cab
The RTU framer overloaded DesyncReason.TruncatedFrame for three distinct failures — a genuine short read, a CRC-16 validation failure, and a CRC-valid reply from the wrong RS-485 slave. Split the latter two into dedicated CrcMismatch / UnitMismatch enum members so a future .Reason consumer (metrics, operator diagnostics) can tell a wiring/EMI CRC fault apart from a mis-addressed multi-drop reply. Behaviour is unchanged — all three still throw ModbusTransportDesyncException and tear the socket down. Framing tests now assert the specific Reason on each path. Follow-up #12 from the Modbus RTU-over-TCP plan. Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
60 lines
2.8 KiB
C#
60 lines
2.8 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
|
|
|
|
/// <summary>
|
|
/// Raised when a Modbus transaction leaves the single-flight socket in an unknown /
|
|
/// desynchronized state — a per-op response timeout, or a framing violation (transaction-id
|
|
/// mismatch, truncated frame, RTU CRC mismatch, or RTU unit-address mismatch). Unlike a Modbus
|
|
/// <em>exception PDU</em> (a well-formed
|
|
/// protocol-level error where the socket is still coherent and the request must simply
|
|
/// propagate), a desync means bytes may still be in flight or half-read, so the socket is
|
|
/// unusable and MUST be torn down before this is thrown.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Derives from <see cref="IOException"/> deliberately: the transport's existing
|
|
/// reconnect-and-retry classifier (<c>IsSocketLevelFailure</c>) already treats
|
|
/// <see cref="IOException"/> as socket-fatal, so with auto-reconnect enabled the transport
|
|
/// transparently reconnects and resends once; with auto-reconnect disabled the (already torn
|
|
/// down) socket is never reused and this surfaces to the driver's status mapping as a
|
|
/// communication error.
|
|
/// </remarks>
|
|
public sealed class ModbusTransportDesyncException : IOException
|
|
{
|
|
/// <summary>The reason the socket was classified desynchronized.</summary>
|
|
public enum DesyncReason
|
|
{
|
|
/// <summary>The per-op deadline (linked <c>CancelAfter</c>) fired — the response never arrived in time.</summary>
|
|
Timeout,
|
|
|
|
/// <summary>The response MBAP transaction id did not match the request's.</summary>
|
|
TxIdMismatch,
|
|
|
|
/// <summary>
|
|
/// The frame ended before it was complete — the MBAP length field was below the mandatory
|
|
/// minimum (TCP), or the stream closed mid-frame while reading a length-less RTU frame.
|
|
/// </summary>
|
|
TruncatedFrame,
|
|
|
|
/// <summary>The RTU frame's trailing CRC-16 did not validate over the received bytes.</summary>
|
|
CrcMismatch,
|
|
|
|
/// <summary>
|
|
/// A CRC-valid RTU frame carried a slave/unit address other than the one addressed — on an
|
|
/// RS-485 multi-drop bus, a reply from the wrong slave.
|
|
/// </summary>
|
|
UnitMismatch,
|
|
}
|
|
|
|
/// <summary>Gets the reason the socket was classified desynchronized.</summary>
|
|
public DesyncReason Reason { get; }
|
|
|
|
/// <summary>Initializes a new instance of the <see cref="ModbusTransportDesyncException"/> class.</summary>
|
|
/// <param name="reason">The desync reason.</param>
|
|
/// <param name="message">A human-readable description.</param>
|
|
/// <param name="inner">The originating exception, if any.</param>
|
|
public ModbusTransportDesyncException(DesyncReason reason, string message, Exception? inner = null)
|
|
: base(message, inner)
|
|
{
|
|
Reason = reason;
|
|
}
|
|
}
|