using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests; public sealed class ModbusRtuFramingTests { private static MemoryStream Canned(params byte[] frame) => new(frame); [Fact] public void BuildAdu_prefixes_unit_and_appends_crc() { var adu = ModbusRtuFraming.BuildAdu(0x01, new byte[] { 0x03, 0x00, 0x00, 0x00, 0x01 }); adu.ShouldBe(new byte[] { 0x01, 0x03, 0x00, 0x00, 0x00, 0x01, 0x84, 0x0A }); } [Fact] public async Task ReadResponse_FC03_returns_bare_pdu() { // addr=01 fc=03 byteCount=02 data=00 0A + CRC(lo,hi) var body = new byte[] { 0x01, 0x03, 0x02, 0x00, 0x0A }; var frame = ModbusCrc.Append(body); await using var s = Canned(frame); var pdu = await ModbusRtuFraming.ReadResponsePduAsync(s, 0x01, TestContext.Current.CancellationToken); pdu.ShouldBe(new byte[] { 0x03, 0x02, 0x00, 0x0A }); // fc + byteCount + data, no addr/CRC } [Fact] public async Task ReadResponse_exception_frame_throws_ModbusException() { // addr=01 fc=0x83 exc=0x02 + CRC var frame = ModbusCrc.Append(new byte[] { 0x01, 0x83, 0x02 }); await using var s = Canned(frame); var ex = await Should.ThrowAsync( ModbusRtuFraming.ReadResponsePduAsync(s, 0x01, TestContext.Current.CancellationToken)); ex.FunctionCode.ShouldBe((byte)0x03); ex.ExceptionCode.ShouldBe((byte)0x02); } [Fact] public async Task ReadResponse_bad_crc_throws_desync() { 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( ModbusRtuFraming.ReadResponsePduAsync(s, 0x01, TestContext.Current.CancellationToken)); } [Fact] public async Task ReadResponse_FC06_write_echo_returns_four_byte_pdu() { // addr=01 fc=06 addr=00 07 value=00 2A + CRC -> PDU = 06 00 07 00 2A var frame = ModbusCrc.Append(new byte[] { 0x01, 0x06, 0x00, 0x07, 0x00, 0x2A }); await using var s = Canned(frame); var pdu = await ModbusRtuFraming.ReadResponsePduAsync(s, 0x01, TestContext.Current.CancellationToken); pdu.ShouldBe(new byte[] { 0x06, 0x00, 0x07, 0x00, 0x2A }); } }