diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusRtuFraming.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusRtuFraming.cs index 7cb765ce..b7f20d05 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusRtuFraming.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusRtuFraming.cs @@ -137,7 +137,7 @@ public static class ModbusRtuFraming var actual = (ushort)(frame[^2] | (frame[^1] << 8)); if (actual != expected) throw new ModbusTransportDesyncException( - ModbusTransportDesyncException.DesyncReason.TruncatedFrame, + ModbusTransportDesyncException.DesyncReason.CrcMismatch, $"Modbus RTU CRC mismatch: computed {expected:X4} got {actual:X4}"); // Unit-address validation runs only AFTER the CRC passes: a corrupt frame is a CRC/desync @@ -146,7 +146,7 @@ public static class ModbusRtuFraming var addr = frame[0]; if (addr != expectedUnit) throw new ModbusTransportDesyncException( - ModbusTransportDesyncException.DesyncReason.TruncatedFrame, + ModbusTransportDesyncException.DesyncReason.UnitMismatch, $"Modbus RTU unit mismatch: expected {expectedUnit} got {addr}"); // Bare PDU = frame minus leading addr and trailing CRC. diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusTransportDesyncException.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusTransportDesyncException.cs index 6f75e270..8a4bd31b 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusTransportDesyncException.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusTransportDesyncException.cs @@ -1,9 +1,10 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus; /// -/// Raised when a Modbus TCP transaction leaves the single-flight socket in an unknown / +/// 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 or truncated MBAP length). Unlike a Modbus exception PDU (a well-formed +/// mismatch, truncated frame, RTU CRC mismatch, or RTU unit-address mismatch). Unlike a Modbus +/// exception PDU (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. @@ -27,8 +28,20 @@ public sealed class ModbusTransportDesyncException : IOException /// The response MBAP transaction id did not match the request's. TxIdMismatch, - /// The response MBAP length field was below the mandatory minimum (truncated frame). + /// + /// 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. + /// TruncatedFrame, + + /// The RTU frame's trailing CRC-16 did not validate over the received bytes. + CrcMismatch, + + /// + /// 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. + /// + UnitMismatch, } /// Gets the reason the socket was classified desynchronized. diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusRtuFramingTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusRtuFramingTests.cs index 44bbb43f..3a26fa28 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusRtuFramingTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusRtuFramingTests.cs @@ -43,8 +43,9 @@ public sealed class ModbusRtuFramingTests var frame = ModbusCrc.Append(new byte[] { 0x01, 0x03, 0x02, 0x00, 0x0A }); frame[^1] ^= 0xFF; // corrupt CRC high byte await using var s = Canned(frame); - await Should.ThrowAsync( + var ex = await Should.ThrowAsync( ModbusRtuFraming.ReadResponsePduAsync(s, 0x01, TestContext.Current.CancellationToken)); + ex.Reason.ShouldBe(ModbusTransportDesyncException.DesyncReason.CrcMismatch); } [Fact] @@ -63,8 +64,9 @@ public sealed class ModbusRtuFramingTests // CRC-valid FC03 frame addressed to unit 0x02, but we asked for 0x01 -> unit-mismatch desync. var frame = ModbusCrc.Append(new byte[] { 0x02, 0x03, 0x02, 0x00, 0x0A }); await using var s = Canned(frame); - await Should.ThrowAsync( + var ex = await Should.ThrowAsync( ModbusRtuFraming.ReadResponsePduAsync(s, 0x01, TestContext.Current.CancellationToken)); + ex.Reason.ShouldBe(ModbusTransportDesyncException.DesyncReason.UnitMismatch); } [Fact] @@ -86,8 +88,9 @@ public sealed class ModbusRtuFramingTests var frame = ModbusCrc.Append(new byte[] { 0x01, 0x03, 0x02, 0x00, 0x0A }); var truncated = frame[..4]; await using var s = new DribbleStream(truncated, chunk: 2); - await Should.ThrowAsync( + var ex = await Should.ThrowAsync( ModbusRtuFraming.ReadResponsePduAsync(s, 0x01, TestContext.Current.CancellationToken)); + ex.Reason.ShouldBe(ModbusTransportDesyncException.DesyncReason.TruncatedFrame); } ///