diff --git a/AMWD.Protocols.Modbus.Common/Protocols/RtuProtocol.cs b/AMWD.Protocols.Modbus.Common/Protocols/RtuProtocol.cs
new file mode 100644
index 0000000..e7c503c
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Protocols/RtuProtocol.cs
@@ -0,0 +1,792 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using AMWD.Protocols.Modbus.Common.Contracts;
+
+namespace AMWD.Protocols.Modbus.Common.Protocols
+{
+ ///
+ /// Default implementation of the Modbus RTU protocol.
+ ///
+ public class RtuProtocol : IModbusProtocol
+ {
+ #region Constants
+
+ ///
+ /// The minimum allowed unit id specified by the Modbus TCP protocol.
+ ///
+ public const byte MIN_UNIT_ID = 0x01;
+
+ ///
+ /// The maximum allowed unit id specified by the Modbus TCP protocol.
+ ///
+ ///
+ /// Reading the specification, the max allowed unit id would be 247!
+ ///
+ public const byte MAX_UNIT_ID = 0xFF;
+
+ ///
+ /// The minimum allowed read count specified by the Modbus TCP protocol.
+ ///
+ public const ushort MIN_READ_COUNT = 0x01;
+
+ ///
+ /// The minimum allowed write count specified by the Modbus TCP protocol.
+ ///
+ public const ushort MIN_WRITE_COUNT = 0x01;
+
+ ///
+ /// The maximum allowed read count for discrete values specified by the Modbus TCP protocol.
+ ///
+ public const ushort MAX_DISCRETE_READ_COUNT = 0x07D0; // 2000
+
+ ///
+ /// The maximum allowed write count for discrete values specified by the Modbus TCP protocol.
+ ///
+ public const ushort MAX_DISCRETE_WRITE_COUNT = 0x07B0; // 1968
+
+ ///
+ /// The maximum allowed read count for registers specified by the Modbus TCP protocol.
+ ///
+ public const ushort MAX_REGISTER_READ_COUNT = 0x007D; // 125
+
+ ///
+ /// The maximum allowed write count for registers specified by the Modbus TCP protocol.
+ ///
+ public const ushort MAX_REGISTER_WRITE_COUNT = 0x007B; // 123
+
+ ///
+ /// The maximum allowed ADU length in bytes.
+ ///
+ ///
+ /// A Modbus frame consists of a PDU (protcol data unit) and additional protocol addressing / error checks.
+ /// The whole data frame is called ADU (application data unit).
+ ///
+ public const int MAX_ADU_LENGTH = 256; // bytes
+
+ #endregion Constants
+
+ ///
+ public string Name => "RTU";
+
+ #region Read
+
+ ///
+ public IReadOnlyList SerializeReadCoils(byte unitId, ushort startAddress, ushort count)
+ {
+ if (unitId < MIN_UNIT_ID)
+ throw new ArgumentOutOfRangeException(nameof(unitId));
+
+ if (count < MIN_READ_COUNT || MAX_DISCRETE_READ_COUNT < count)
+ throw new ArgumentOutOfRangeException(nameof(count));
+
+ if (ushort.MaxValue < (startAddress + count - 1))
+ throw new ArgumentOutOfRangeException(nameof(count), $"Combination of {nameof(startAddress)} and {nameof(count)} exceeds the addressation limit of {ushort.MaxValue}");
+
+ byte[] request = new byte[8];
+
+ // Unit Id
+ request[0] = unitId;
+
+ // Function code
+ request[1] = (byte)ModbusFunctionCode.ReadCoils;
+
+ // Starting address
+ byte[] addrBytes = startAddress.ToBigEndianBytes();
+ request[2] = addrBytes[0];
+ request[3] = addrBytes[1];
+
+ // Quantity
+ byte[] countBytes = count.ToBigEndianBytes();
+ request[4] = countBytes[0];
+ request[5] = countBytes[1];
+
+ // CRC
+ byte[] crc = CRC16(request, 0, 6);
+ request[6] = crc[0];
+ request[7] = crc[1];
+
+ return request;
+ }
+
+ ///
+ public IReadOnlyList DeserializeReadCoils(IReadOnlyList response)
+ {
+ int baseOffset = 3;
+ if (response[2] != response.Count - baseOffset - 2) // -2 for CRC
+ throw new ModbusException("Coil byte count does not match.");
+
+ int count = response[2] * 8;
+ var coils = new List();
+ for (int i = 0; i < count; i++)
+ {
+ int bytePosition = i / 8;
+ int bitPosition = i % 8;
+
+ int value = response[baseOffset + bytePosition] & (1 << bitPosition);
+ coils.Add(new Coil
+ {
+ Address = (ushort)i,
+ Value = value > 0
+ });
+ }
+
+ return coils;
+ }
+
+ ///
+ public IReadOnlyList SerializeReadDiscreteInputs(byte unitId, ushort startAddress, ushort count)
+ {
+ if (unitId < MIN_UNIT_ID)
+ throw new ArgumentOutOfRangeException(nameof(unitId));
+
+ if (count < MIN_READ_COUNT || MAX_DISCRETE_READ_COUNT < count)
+ throw new ArgumentOutOfRangeException(nameof(count));
+
+ if (ushort.MaxValue < (startAddress + count - 1))
+ throw new ArgumentOutOfRangeException(nameof(count), $"Combination of {nameof(startAddress)} and {nameof(count)} exceeds the addressation limit of {ushort.MaxValue}");
+
+ byte[] request = new byte[8];
+
+ // Unit Id
+ request[0] = unitId;
+
+ // Function code
+ request[1] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
+
+ // Starting address
+ byte[] addrBytes = startAddress.ToBigEndianBytes();
+ request[2] = addrBytes[0];
+ request[3] = addrBytes[1];
+
+ // Quantity
+ byte[] countBytes = count.ToBigEndianBytes();
+ request[4] = countBytes[0];
+ request[5] = countBytes[1];
+
+ // CRC
+ byte[] crc = CRC16(request, 0, 6);
+ request[6] = crc[0];
+ request[7] = crc[1];
+
+ return request;
+ }
+
+ ///
+ public IReadOnlyList DeserializeReadDiscreteInputs(IReadOnlyList response)
+ {
+ int baseOffset = 3;
+ if (response[2] != response.Count - baseOffset - 2) // -2 for CRC
+ throw new ModbusException("Discrete input byte count does not match.");
+
+ int count = response[2] * 8;
+ var discreteInputs = new List();
+ for (int i = 0; i < count; i++)
+ {
+ int bytePosition = i / 8;
+ int bitPosition = i % 8;
+
+ int value = response[baseOffset + bytePosition] & (1 << bitPosition);
+ discreteInputs.Add(new DiscreteInput
+ {
+ Address = (ushort)i,
+ HighByte = (byte)(value > 0 ? 0xFF : 0x00)
+ });
+ }
+
+ return discreteInputs;
+ }
+
+ ///
+ public IReadOnlyList SerializeReadHoldingRegisters(byte unitId, ushort startAddress, ushort count)
+ {
+ if (unitId < MIN_UNIT_ID)
+ throw new ArgumentOutOfRangeException(nameof(unitId));
+
+ if (count < MIN_READ_COUNT || MAX_REGISTER_READ_COUNT < count)
+ throw new ArgumentOutOfRangeException(nameof(count));
+
+ if (ushort.MaxValue < (startAddress + count - 1))
+ throw new ArgumentOutOfRangeException(nameof(count), $"Combination of {nameof(startAddress)} and {nameof(count)} exceeds the addressation limit of {ushort.MaxValue}");
+
+ byte[] request = new byte[8];
+
+ // Unit Id
+ request[0] = unitId;
+
+ // Function code
+ request[1] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
+
+ // Starting address
+ byte[] addrBytes = startAddress.ToBigEndianBytes();
+ request[2] = addrBytes[0];
+ request[3] = addrBytes[1];
+
+ // Quantity
+ byte[] countBytes = count.ToBigEndianBytes();
+ request[4] = countBytes[0];
+ request[5] = countBytes[1];
+
+ // CRC
+ byte[] crc = CRC16(request, 0, 6);
+ request[6] = crc[0];
+ request[7] = crc[1];
+
+ return request;
+ }
+
+ ///
+ public IReadOnlyList DeserializeReadHoldingRegisters(IReadOnlyList response)
+ {
+ int baseOffset = 3;
+ if (response[2] != response.Count - baseOffset - 2)
+ throw new ModbusException("Holding register byte count does not match.");
+
+ int count = response[2] / 2;
+ var holdingRegisters = new List();
+ for (int i = 0; i < count; i++)
+ {
+ holdingRegisters.Add(new HoldingRegister
+ {
+ Address = (ushort)i,
+ HighByte = response[baseOffset + i * 2],
+ LowByte = response[baseOffset + i * 2 + 1]
+ });
+ }
+
+ return holdingRegisters;
+ }
+
+ ///
+ public IReadOnlyList SerializeReadInputRegisters(byte unitId, ushort startAddress, ushort count)
+ {
+ if (unitId < MIN_UNIT_ID)
+ throw new ArgumentOutOfRangeException(nameof(unitId));
+
+ if (count < MIN_READ_COUNT || MAX_REGISTER_READ_COUNT < count)
+ throw new ArgumentOutOfRangeException(nameof(count));
+
+ if (ushort.MaxValue < (startAddress + count - 1))
+ throw new ArgumentOutOfRangeException(nameof(count), $"Combination of {nameof(startAddress)} and {nameof(count)} exceeds the addressation limit of {ushort.MaxValue}");
+
+ byte[] request = new byte[8];
+
+ // Unit Id
+ request[0] = unitId;
+
+ // Function code
+ request[1] = (byte)ModbusFunctionCode.ReadInputRegisters;
+
+ // Starting address
+ byte[] addrBytes = startAddress.ToBigEndianBytes();
+ request[2] = addrBytes[0];
+ request[3] = addrBytes[1];
+
+ // Quantity
+ byte[] countBytes = count.ToBigEndianBytes();
+ request[4] = countBytes[0];
+ request[5] = countBytes[1];
+
+ // CRC
+ byte[] crc = CRC16(request, 0, 6);
+ request[6] = crc[0];
+ request[7] = crc[1];
+
+ return request;
+ }
+
+ ///
+ public IReadOnlyList DeserializeReadInputRegisters(IReadOnlyList response)
+ {
+ int baseOffset = 3;
+ if (response[2] != response.Count - baseOffset - 2)
+ throw new ModbusException("Input register byte count does not match.");
+
+ int count = response[2] / 2;
+ var inputRegisters = new List();
+ for (int i = 0; i < count; i++)
+ {
+ inputRegisters.Add(new InputRegister
+ {
+ Address = (ushort)i,
+ HighByte = response[baseOffset + i * 2],
+ LowByte = response[baseOffset + i * 2 + 1]
+ });
+ }
+
+ return inputRegisters;
+ }
+
+ ///
+ public IReadOnlyList SerializeReadDeviceIdentification(byte unitId, ModbusDeviceIdentificationCategory category, ModbusDeviceIdentificationObject objectId)
+ {
+ if (unitId < MIN_UNIT_ID)
+ throw new ArgumentOutOfRangeException(nameof(unitId));
+
+ if (!Enum.IsDefined(typeof(ModbusDeviceIdentificationCategory), category))
+ throw new ArgumentOutOfRangeException(nameof(category));
+
+ byte[] request = new byte[7];
+
+ // Unit Id
+ request[0] = unitId;
+
+ // Function code
+ request[1] = (byte)ModbusFunctionCode.EncapsulatedInterface;
+
+ // Modbus Encapsulated Interface: Read Device Identification (MEI Type)
+ request[2] = 0x0E;
+
+ // The category type (basic, regular, extended, individual)
+ request[3] = (byte)category;
+ request[4] = (byte)objectId;
+
+ // CRC
+ byte[] crc = CRC16(request, 0, 5);
+ request[5] = crc[0];
+ request[6] = crc[1];
+
+ return request;
+ }
+
+ ///
+ public DeviceIdentificationRaw DeserializeReadDeviceIdentification(IReadOnlyList response)
+ {
+ if (response[2] != 0x0E)
+ throw new ModbusException("The MEI type does not match");
+
+ if (!Enum.IsDefined(typeof(ModbusDeviceIdentificationCategory), response[3]))
+ throw new ModbusException("The category type does not match");
+
+ var deviceIdentification = new DeviceIdentificationRaw
+ {
+ AllowsIndividualAccess = (response[4] & 0x80) == 0x80,
+ MoreRequestsNeeded = response[5] == 0xFF,
+ NextObjectIdToRequest = response[6],
+ };
+
+ int baseOffset = 8;
+ while (baseOffset < response.Count - 2) // -2 for CRC
+ {
+ byte objectId = response[baseOffset];
+ byte length = response[baseOffset + 1];
+
+ byte[] data = response.Skip(baseOffset + 2).Take(length).ToArray();
+
+ deviceIdentification.Objects.Add(objectId, data);
+ baseOffset += 2 + length;
+ }
+
+ return deviceIdentification;
+ }
+
+ #endregion Read
+
+ #region Write
+
+ ///
+ public IReadOnlyList SerializeWriteSingleCoil(byte unitId, Coil coil)
+ {
+ if (unitId < MIN_UNIT_ID)
+ throw new ArgumentOutOfRangeException(nameof(unitId));
+
+#if NET8_0_OR_GREATER
+ ArgumentNullException.ThrowIfNull(coil);
+#else
+ if (coil == null)
+ throw new ArgumentNullException(nameof(coil));
+#endif
+
+ byte[] request = new byte[8];
+
+ // Unit ID
+ request[0] = unitId;
+
+ // Function code
+ request[1] = (byte)ModbusFunctionCode.WriteSingleCoil;
+
+ byte[] addrBytes = coil.Address.ToBigEndianBytes();
+ request[2] = addrBytes[0];
+ request[3] = addrBytes[1];
+
+ request[4] = coil.HighByte;
+ request[5] = coil.LowByte;
+
+ // CRC
+ byte[] crc = CRC16(request, 0, 6);
+ request[6] = crc[0];
+ request[7] = crc[1];
+
+ return request;
+ }
+
+ ///
+ public Coil DeserializeWriteSingleCoil(IReadOnlyList response)
+ {
+ return new Coil
+ {
+ Address = response.ToArray().GetBigEndianUInt16(2),
+ HighByte = response[4],
+ LowByte = response[5]
+ };
+ }
+
+ ///
+ public IReadOnlyList SerializeWriteSingleHoldingRegister(byte unitId, HoldingRegister register)
+ {
+ if (unitId < MIN_UNIT_ID)
+ throw new ArgumentOutOfRangeException(nameof(unitId));
+
+#if NET8_0_OR_GREATER
+ ArgumentNullException.ThrowIfNull(register);
+#else
+ if (register == null)
+ throw new ArgumentNullException(nameof(register));
+#endif
+
+ byte[] request = new byte[8];
+
+ // Unit Id
+ request[0] = unitId;
+
+ // Function code
+ request[1] = (byte)ModbusFunctionCode.WriteSingleRegister;
+
+ byte[] addrBytes = register.Address.ToBigEndianBytes();
+ request[2] = addrBytes[0];
+ request[3] = addrBytes[1];
+
+ request[4] = register.HighByte;
+ request[5] = register.LowByte;
+
+ // CRC
+ byte[] crc = CRC16(request, 0, 6);
+ request[6] = crc[0];
+ request[7] = crc[1];
+
+ return request;
+ }
+
+ ///
+ public HoldingRegister DeserializeWriteSingleHoldingRegister(IReadOnlyList response)
+ {
+ return new HoldingRegister
+ {
+ Address = response.ToArray().GetBigEndianUInt16(2),
+ HighByte = response[4],
+ LowByte = response[5]
+ };
+ }
+
+ ///
+ public IReadOnlyList SerializeWriteMultipleCoils(byte unitId, IReadOnlyList coils)
+ {
+ if (unitId < MIN_UNIT_ID)
+ throw new ArgumentOutOfRangeException(nameof(unitId));
+
+#if NET8_0_OR_GREATER
+ ArgumentNullException.ThrowIfNull(coils);
+#else
+ if (coils == null)
+ throw new ArgumentNullException(nameof(coils));
+#endif
+
+ var orderedList = coils.OrderBy(c => c.Address).ToList();
+ if (orderedList.Count < MIN_WRITE_COUNT || MAX_DISCRETE_WRITE_COUNT < orderedList.Count)
+ throw new ArgumentOutOfRangeException(nameof(coils), $"At least {MIN_WRITE_COUNT} or max. {MAX_DISCRETE_WRITE_COUNT} coils can be written at once.");
+
+ int addrCount = coils.Select(c => c.Address).Distinct().Count();
+ if (orderedList.Count != addrCount)
+ throw new ArgumentException("One or more duplicate coils found.", nameof(coils));
+
+ ushort firstAddress = orderedList.First().Address;
+ ushort lastAddress = orderedList.Last().Address;
+
+ if (firstAddress + orderedList.Count - 1 != lastAddress)
+ throw new ArgumentException("Gap in coil list found.", nameof(coils));
+
+ byte byteCount = (byte)Math.Ceiling(orderedList.Count / 8.0);
+ byte[] request = new byte[9 + byteCount];
+
+ request[0] = unitId;
+
+ request[1] = (byte)ModbusFunctionCode.WriteMultipleCoils;
+
+ byte[] addrBytes = firstAddress.ToBigEndianBytes();
+ request[2] = addrBytes[0];
+ request[3] = addrBytes[1];
+
+ byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
+ request[4] = countBytes[0];
+ request[5] = countBytes[1];
+
+ request[6] = byteCount;
+
+ int baseOffset = 7;
+ for (int i = 0; i < orderedList.Count; i++)
+ {
+ int bytePosition = i / 8;
+ int bitPosition = i % 8;
+
+ if (orderedList[i].Value)
+ {
+ byte bitMask = (byte)(1 << bitPosition);
+ request[baseOffset + bytePosition] |= bitMask;
+ }
+ }
+
+ // CRC
+ byte[] crc = CRC16(request, 0, request.Length - 2);
+ request[request.Length - 2] = crc[0];
+ request[request.Length - 1] = crc[1];
+
+ return request;
+ }
+
+ ///
+ public (ushort FirstAddress, ushort NumberOfCoils) DeserializeWriteMultipleCoils(IReadOnlyList response)
+ {
+ ushort firstAddress = response.ToArray().GetBigEndianUInt16(2);
+ ushort numberOfCoils = response.ToArray().GetBigEndianUInt16(4);
+
+ return (firstAddress, numberOfCoils);
+ }
+
+ ///
+ public IReadOnlyList SerializeWriteMultipleHoldingRegisters(byte unitId, IReadOnlyList registers)
+ {
+ if (unitId < MIN_UNIT_ID)
+ throw new ArgumentOutOfRangeException(nameof(unitId));
+
+#if NET8_0_OR_GREATER
+ ArgumentNullException.ThrowIfNull(registers);
+#else
+ if (registers == null)
+ throw new ArgumentNullException(nameof(registers));
+#endif
+
+ var orderedList = registers.OrderBy(c => c.Address).ToList();
+ if (orderedList.Count < MIN_WRITE_COUNT || MAX_REGISTER_WRITE_COUNT < orderedList.Count)
+ throw new ArgumentOutOfRangeException(nameof(registers), $"At least {MIN_WRITE_COUNT} or max. {MAX_REGISTER_WRITE_COUNT} holding registers can be written at once.");
+
+ int addrCount = registers.Select(c => c.Address).Distinct().Count();
+ if (orderedList.Count != addrCount)
+ throw new ArgumentException("One or more duplicate holding registers found.", nameof(registers));
+
+ ushort firstAddress = orderedList.First().Address;
+ ushort lastAddress = orderedList.Last().Address;
+
+ if (firstAddress + orderedList.Count - 1 != lastAddress)
+ throw new ArgumentException("Gap in holding register list found.", nameof(registers));
+
+ byte byteCount = (byte)(orderedList.Count * 2);
+ byte[] request = new byte[9 + byteCount];
+
+ request[0] = unitId;
+ request[1] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
+
+ byte[] addrBytes = firstAddress.ToBigEndianBytes();
+ request[2] = addrBytes[0];
+ request[3] = addrBytes[1];
+
+ byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
+ request[4] = countBytes[0];
+ request[5] = countBytes[1];
+
+ request[6] = byteCount;
+
+ int baseOffset = 7;
+ for (int i = 0; i < orderedList.Count; i++)
+ {
+ request[baseOffset + 2 * i] = orderedList[i].HighByte;
+ request[baseOffset + 2 * i + 1] = orderedList[i].LowByte;
+ }
+
+ // CRC
+ byte[] crc = CRC16(request, 0, request.Length - 2);
+ request[request.Length - 2] = crc[0];
+ request[request.Length - 1] = crc[1];
+
+ return request;
+ }
+
+ ///
+ public (ushort FirstAddress, ushort NumberOfRegisters) DeserializeWriteMultipleHoldingRegisters(IReadOnlyList response)
+ {
+ ushort firstAddress = response.ToArray().GetBigEndianUInt16(2);
+ ushort numberOfRegisters = response.ToArray().GetBigEndianUInt16(4);
+
+ return (firstAddress, numberOfRegisters);
+ }
+
+ #endregion Write
+
+ #region Validation
+
+ ///
+ public bool CheckResponseComplete(IReadOnlyList responseBytes)
+ {
+ // Minimum requirement => Unit ID, Function code and at least 2x CRC
+ if (responseBytes.Count < 4)
+ return false;
+
+ // Response is error response
+ if ((responseBytes[1] & 0x80) == 0x80)
+ {
+ // Unit ID, Function Code, ErrorCode, 2x CRC
+ if (responseBytes.Count < 5)
+ return false;
+
+ // On error, skip any other evaluation
+ return true;
+ }
+
+ // Read responses
+ // - 0x01 Read Coils
+ // - 0x02 Read Discrete Inputs
+ // - 0x03 Read Holding Registers
+ // - 0x04 Read Input Registers
+ // do have a "following bytes" at position 3
+ if (new[] { 0x01, 0x02, 0x03, 0x04 }.Contains(responseBytes[1]))
+ {
+ // Unit ID, Function Code, ByteCount, 2x CRC and length of ByteCount
+ if (responseBytes.Count < 5 + responseBytes[2])
+ return false;
+ }
+
+ // - 0x05 Write Single Coil
+ // - 0x06 Write Single Register
+ // - 0x0F Write Multiple Coils
+ // - 0x10 Write Multiple Registers
+ if (new[] { 0x05, 0x06, 0x0F, 0x10 }.Contains(responseBytes[1]))
+ {
+ // Write Single => Unit ID, Function code, 2x Address, 2x Value, 2x CRC
+ // Write Multi => Unit ID, Function code, 2x Address, 2x QuantityWritten, 2x CRC
+ if (responseBytes.Count < 8)
+ return false;
+ }
+
+ // 0x2B Read Device Identification
+ if (responseBytes[1] == 0x2B)
+ {
+ // [0] 1x Unit ID
+ // [1] 1x Function code
+ // [2] 1x MEI Type
+ // [3] 1x Category
+ // [4] 1x Conformity Level
+ // [5] 1x More Follows
+ // [6] 1x Next Object ID
+ // [7] 1x NumberOfObjects
+ // ----- repeat NumberOfObjects times
+ // 1x Object ID
+ // 1x length N
+ // Nx data
+ // -----
+ // 2x CRC
+
+ if (responseBytes.Count < 8)
+ return false;
+
+ byte numberOfObjects = responseBytes[7];
+ if (numberOfObjects == 0)
+ {
+ if (responseBytes.Count < 10)
+ return false;
+
+ return true;
+ }
+
+ int offset = 8;
+ for (int i = 0; i < numberOfObjects; i++)
+ {
+ offset++; // object id
+ byte length = responseBytes[offset++];
+ offset += length; // data
+
+ // 2x CRC or next object ID and length
+ if (responseBytes.Count < offset + 2)
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ public void ValidateResponse(IReadOnlyList request, IReadOnlyList response)
+ {
+ if (request[0] != response[0])
+ throw new ModbusException("Unit Identifier does not match.");
+
+ byte[] calculatedCrc16 = CRC16(response, 0, response.Count - 2);
+ byte[] receivedCrc16 = [response[response.Count - 2], response[response.Count - 1]];
+
+ if (calculatedCrc16[0] != receivedCrc16[0] || calculatedCrc16[1] != receivedCrc16[1])
+ throw new ModbusException("CRC16 check failed.");
+
+ byte fnCode = response[1];
+ bool isError = (fnCode & 0x80) == 0x80;
+ if (isError)
+ fnCode = (byte)(fnCode ^ 0x80); // === fnCode & 0x7F
+
+ if (request[1] != fnCode)
+ throw new ModbusException("Function code does not match.");
+
+ if (isError)
+ throw new ModbusException("Remote Error") { ErrorCode = (ModbusErrorCode)response[2] };
+
+ if (new[] { 0x01, 0x02, 0x03, 0x04 }.Contains(fnCode))
+ {
+ if (response.Count != 5 + response[2])
+ throw new ModbusException("Number of following bytes does not match.");
+ }
+
+ if (new[] { 0x05, 0x06, 0x0F, 0x10 }.Contains(fnCode))
+ {
+ if (response.Count != 8)
+ throw new ModbusException("Number of bytes does not match.");
+ }
+
+ // TODO: Do we want to check 0x2B too?
+ }
+
+ ///
+ /// Calculate CRC16 for Modbus RTU.
+ ///
+ /// The message bytes.
+ /// The start index.
+ /// The number of bytes to calculate.
+ public static byte[] CRC16(IReadOnlyList bytes, int start = 0, int? length = null)
+ {
+ if (bytes == null || bytes.Count == 0)
+ throw new ArgumentNullException(nameof(bytes));
+
+ if (start < 0 || start >= bytes.Count)
+ throw new ArgumentOutOfRangeException(nameof(start));
+
+ length ??= bytes.Count - start;
+
+ if (length <= 0 || start + length > bytes.Count)
+ throw new ArgumentOutOfRangeException(nameof(length));
+
+ byte lsb;
+ ushort crc16 = 0xFFFF;
+ for (int i = start; i < start + length; i++)
+ {
+ crc16 = (ushort)(crc16 ^ bytes[i]);
+ for (int j = 0; j < 8; j++)
+ {
+ lsb = (byte)(crc16 & 0x0001);
+ crc16 = (ushort)(crc16 >> 1);
+
+ if (lsb == 0x01)
+ crc16 = (ushort)(crc16 ^ 0xA001);
+ }
+ }
+
+ return [(byte)crc16, (byte)(crc16 >> 8)];
+ }
+
+ #endregion Validation
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Tests/Common/Protocols/RtuProtocolTest.cs b/AMWD.Protocols.Modbus.Tests/Common/Protocols/RtuProtocolTest.cs
new file mode 100644
index 0000000..f9f06e7
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Tests/Common/Protocols/RtuProtocolTest.cs
@@ -0,0 +1,1390 @@
+using System.Collections.Generic;
+using System.Text;
+using AMWD.Protocols.Modbus.Common.Protocols;
+
+namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
+{
+ [TestClass]
+ public class RtuProtocolTest
+ {
+ private const byte UNIT_ID = 0x2A; // 42
+
+ #region Read Coils
+
+ [TestMethod]
+ public void ShouldSerializeReadCoils()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ var bytes = protocol.SerializeReadCoils(UNIT_ID, 19, 19);
+
+ // Assert
+ Assert.IsNotNull(bytes);
+ Assert.AreEqual(8, bytes.Count);
+
+ // Unit id
+ Assert.AreEqual(UNIT_ID, bytes[0]);
+
+ // Function code
+ Assert.AreEqual(0x01, bytes[1]);
+
+ // Starting address
+ Assert.AreEqual(0x00, bytes[2]);
+ Assert.AreEqual(0x13, bytes[3]);
+ // Quantity
+ Assert.AreEqual(0x00, bytes[4]);
+ Assert.AreEqual(0x13, bytes[5]);
+
+ // CRC check will be ignored
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForUnitIdOnSerializeReadCoils()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeReadCoils(0x00, 19, 19);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [DataTestMethod]
+ [DataRow(0)]
+ [DataRow(2001)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForCountOnSerializeReadCoils(int count)
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeReadCoils(UNIT_ID, 19, (ushort)count);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadCoils()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeReadCoils(UNIT_ID, ushort.MaxValue, 2);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ public void ShouldDeserializeReadCoils()
+ {
+ // Arrange
+ int[] setValues = [0, 2, 3, 6, 7, 8, 9, 11, 13, 14, 16, 18];
+ var protocol = new RtuProtocol();
+
+ // Act
+ var coils = protocol.DeserializeReadCoils([UNIT_ID, 0x01, 0x03, 0xCD, 0x6B, 0x05, 0x00, 0x00]);
+
+ // Assert
+ Assert.IsNotNull(coils);
+ Assert.AreEqual(24, coils.Count);
+
+ for (int i = 0; i < 24; i++)
+ {
+ Assert.AreEqual(i, coils[i].Address);
+
+ if (setValues.Contains(i))
+ Assert.IsTrue(coils[i].Value);
+ else
+ Assert.IsFalse(coils[i].Value);
+ }
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowExceptionOnDeserializeReadCoils()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ _ = protocol.DeserializeReadCoils([UNIT_ID, 0x01, 0x02, 0xCD, 0x6B, 0x05, 0x00, 0x00]);
+
+ // Assert - ModbusException
+ }
+
+ #endregion Read Coils
+
+ #region Read Discrete Inputs
+
+ [TestMethod]
+ public void ShouldSerializeReadDiscreteInputs()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ var bytes = protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, 19);
+
+ // Assert
+ Assert.IsNotNull(bytes);
+ Assert.AreEqual(8, bytes.Count);
+
+ // Unit id
+ Assert.AreEqual(UNIT_ID, bytes[0]);
+
+ // Function code
+ Assert.AreEqual(0x02, bytes[1]);
+
+ // Starting address
+ Assert.AreEqual(0x00, bytes[2]);
+ Assert.AreEqual(0x13, bytes[3]);
+ // Quantity
+ Assert.AreEqual(0x00, bytes[4]);
+ Assert.AreEqual(0x13, bytes[5]);
+
+ // CRC check will be ignored
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForUnitIdOnSerializeReadDiscreteInputs()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeReadDiscreteInputs(0x00, 19, 19);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [DataTestMethod]
+ [DataRow(0)]
+ [DataRow(2001)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForCountOnSerializeReadDiscreteInputs(int count)
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, (ushort)count);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadDiscreteInputs()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeReadDiscreteInputs(UNIT_ID, ushort.MaxValue, 2);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ public void ShouldDeserializeReadDiscreteInputs()
+ {
+ // Arrange
+ int[] setValues = [0, 2, 3, 6, 7, 8, 9, 11, 13, 14, 16, 18];
+ var protocol = new RtuProtocol();
+
+ // Act
+ var coils = protocol.DeserializeReadDiscreteInputs([UNIT_ID, 0x02, 0x03, 0xCD, 0x6B, 0x05, 0x00, 0x00]);
+
+ // Assert
+ Assert.IsNotNull(coils);
+ Assert.AreEqual(24, coils.Count);
+
+ for (int i = 0; i < 24; i++)
+ {
+ Assert.AreEqual(i, coils[i].Address);
+
+ if (setValues.Contains(i))
+ Assert.IsTrue(coils[i].Value);
+ else
+ Assert.IsFalse(coils[i].Value);
+ }
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowExceptionOnDeserializeReadDiscreteInputs()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ _ = protocol.DeserializeReadDiscreteInputs([UNIT_ID, 0x02, 0x02, 0xCD, 0x6B, 0x05, 0x00, 0x00]);
+
+ // Assert - ModbusException
+ }
+
+ #endregion Read Discrete Inputs
+
+ #region Read Holding Registers
+
+ [TestMethod]
+ public void ShouldSerializeReadHoldingRegisters()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ var bytes = protocol.SerializeReadHoldingRegisters(UNIT_ID, 107, 2);
+
+ // Assert
+ Assert.IsNotNull(bytes);
+ Assert.AreEqual(8, bytes.Count);
+
+ // Unit id
+ Assert.AreEqual(UNIT_ID, bytes[0]);
+
+ // Function code
+ Assert.AreEqual(0x03, bytes[1]);
+
+ // Starting address
+ Assert.AreEqual(0x00, bytes[2]);
+ Assert.AreEqual(0x6B, bytes[3]);
+ // Quantity
+ Assert.AreEqual(0x00, bytes[4]);
+ Assert.AreEqual(0x02, bytes[5]);
+
+ // CRC check will be ignored
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForUnitIdOnSerializeReadHoldingRegisters()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeReadHoldingRegisters(0x00, 19, 19);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [DataTestMethod]
+ [DataRow(0)]
+ [DataRow(126)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForCountOnSerializeReadHoldingRegisters(int count)
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeReadHoldingRegisters(UNIT_ID, 19, (ushort)count);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadHoldingRegisters()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeReadHoldingRegisters(UNIT_ID, ushort.MaxValue, 2);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ public void ShouldDeserializeReadHoldingRegisters()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ var registers = protocol.DeserializeReadHoldingRegisters([UNIT_ID, 0x03, 0x04, 0x02, 0x2B, 0x00, 0x64, 0x00, 0x00]);
+
+ // Assert
+ Assert.IsNotNull(registers);
+ Assert.AreEqual(2, registers.Count);
+
+ Assert.AreEqual(0, registers[0].Address);
+ Assert.AreEqual(555, registers[0].Value);
+
+ Assert.AreEqual(1, registers[1].Address);
+ Assert.AreEqual(100, registers[1].Value);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowExceptionOnDeserializeReadHoldingRegisters()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.DeserializeReadHoldingRegisters([UNIT_ID, 0x03, 0x04, 0x02, 0x2B, 0x00, 0x00]);
+
+ // Assert - ModbusException
+ }
+
+ #endregion Read Holding Registers
+
+ #region Read Input Registers
+
+ [TestMethod]
+ public void ShouldSerializeReadInputRegisters()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ var bytes = protocol.SerializeReadInputRegisters(UNIT_ID, 107, 2);
+
+ // Assert
+ Assert.IsNotNull(bytes);
+ Assert.AreEqual(8, bytes.Count);
+
+ // Unit id
+ Assert.AreEqual(UNIT_ID, bytes[0]);
+
+ // Function code
+ Assert.AreEqual(0x04, bytes[1]);
+
+ // Starting address
+ Assert.AreEqual(0x00, bytes[2]);
+ Assert.AreEqual(0x6B, bytes[3]);
+ // Quantity
+ Assert.AreEqual(0x00, bytes[4]);
+ Assert.AreEqual(0x02, bytes[5]);
+
+ // CRC check will be ignored
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForUnitIdOnSerializeReadInputRegisters()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeReadInputRegisters(0x00, 19, 19);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [DataTestMethod]
+ [DataRow(0)]
+ [DataRow(126)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForCountOnSerializeReadInputRegisters(int count)
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeReadInputRegisters(UNIT_ID, 19, (ushort)count);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadInputRegisters()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeReadInputRegisters(UNIT_ID, ushort.MaxValue, 2);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ public void ShouldDeserializeReadInputRegisters()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ var registers = protocol.DeserializeReadInputRegisters([UNIT_ID, 0x04, 0x04, 0x02, 0x2B, 0x00, 0x64, 0x00, 0x00]);
+
+ // Assert
+ Assert.IsNotNull(registers);
+ Assert.AreEqual(2, registers.Count);
+
+ Assert.AreEqual(0, registers[0].Address);
+ Assert.AreEqual(555, registers[0].Value);
+
+ Assert.AreEqual(1, registers[1].Address);
+ Assert.AreEqual(100, registers[1].Value);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowExceptionOnDeserializeReadInputRegisters()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.DeserializeReadInputRegisters([UNIT_ID, 0x04, 0x04, 0x02, 0x2B, 0x00, 0x00]);
+
+ // Assert - ModbusException
+ }
+
+ #endregion Read Input Registers
+
+ #region Read Device Identification
+
+ [DataTestMethod]
+ [DataRow(ModbusDeviceIdentificationCategory.Basic)]
+ [DataRow(ModbusDeviceIdentificationCategory.Regular)]
+ [DataRow(ModbusDeviceIdentificationCategory.Extended)]
+ [DataRow(ModbusDeviceIdentificationCategory.Individual)]
+ public void ShouldSerializeReadDeviceIdentification(ModbusDeviceIdentificationCategory category)
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ var bytes = protocol.SerializeReadDeviceIdentification(UNIT_ID, category, ModbusDeviceIdentificationObject.ProductCode);
+
+ // Assert
+ Assert.IsNotNull(bytes);
+ Assert.AreEqual(7, bytes.Count);
+
+ // Unit id
+ Assert.AreEqual(UNIT_ID, bytes[0]);
+
+ // Function code
+ Assert.AreEqual(0x2B, bytes[1]);
+
+ // MEI Type
+ Assert.AreEqual(0x0E, bytes[2]);
+
+ // Category
+ Assert.AreEqual((byte)category, bytes[3]);
+
+ // Object Id
+ Assert.AreEqual((byte)ModbusDeviceIdentificationObject.ProductCode, bytes[4]);
+
+ // CRC check will be ignored
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeExceptionForUnitIdOnSerializeReadDeviceIdentification()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeReadDeviceIdentification(0x00, ModbusDeviceIdentificationCategory.Basic, ModbusDeviceIdentificationObject.ProductCode);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeExceptionForCategoryOnSerializeReadDeviceIdentification()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeReadDeviceIdentification(UNIT_ID, (ModbusDeviceIdentificationCategory)10, ModbusDeviceIdentificationObject.ProductCode);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [DataTestMethod]
+ [DataRow(false)]
+ [DataRow(true)]
+ public void ShouldDeserializeReadDeviceIdentification(bool moreAndIndividual)
+ {
+ // Arrange
+ byte[] response = [UNIT_ID, 0x2B, 0x0E, 0x02, (byte)(moreAndIndividual ? 0x82 : 0x02), (byte)(moreAndIndividual ? 0xFF : 0x00), (byte)(moreAndIndividual ? 0x05 : 0x00), 0x01, 0x04, 0x02, 0x41, 0x4D, 0x00, 0x00];
+ var protocol = new RtuProtocol();
+
+ // Act
+ var result = protocol.DeserializeReadDeviceIdentification(response);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual(moreAndIndividual, result.AllowsIndividualAccess);
+ Assert.AreEqual(moreAndIndividual, result.MoreRequestsNeeded);
+ Assert.AreEqual(moreAndIndividual ? 0x05 : 0x00, result.NextObjectIdToRequest);
+
+ Assert.AreEqual(1, result.Objects.Count);
+ Assert.AreEqual(4, result.Objects.First().Key);
+ CollectionAssert.AreEqual("AM"u8.ToArray(), result.Objects.First().Value);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForMeiType()
+ {
+ // Arrange
+ byte[] response = [UNIT_ID, 0x2B, 0x0D, 0x00, 0x00];
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.DeserializeReadDeviceIdentification(response);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForCategory()
+ {
+ // Arrange
+ byte[] response = [UNIT_ID, 0x2B, 0x0E, 0x08, 0x00, 0x00];
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.DeserializeReadDeviceIdentification(response);
+ }
+
+ #endregion Read Device Identification
+
+ #region Write Single Coil
+
+ [TestMethod]
+ public void ShouldSerializeWriteSingleCoil()
+ {
+ // Arrange
+ var coil = new Coil { Address = 109, Value = true };
+ var protocol = new RtuProtocol();
+
+ // Act
+ var result = protocol.SerializeWriteSingleCoil(UNIT_ID, coil);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual(8, result.Count);
+
+ // Unit id
+ Assert.AreEqual(UNIT_ID, result[0]);
+
+ // Function code
+ Assert.AreEqual(0x05, result[1]);
+
+ // Starting address
+ Assert.AreEqual(0x00, result[2]);
+ Assert.AreEqual(0x6D, result[3]);
+
+ // Value
+ Assert.AreEqual(0xFF, result[4]);
+ Assert.AreEqual(0x00, result[5]);
+
+ // CRC check will be ignored
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowArgumentOutOfRangeForUnitIdOnSerializeWriteSingleCoil()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeWriteSingleCoil(0x00, new Coil());
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public void ShouldThrowArgumentNullOnSerializeWriteSingleCoil()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeWriteSingleCoil(UNIT_ID, null);
+
+ // Assert - ArgumentNullException
+ }
+
+ [TestMethod]
+ public void ShouldDeserializeWriteSingleCoil()
+ {
+ // Arrange
+ byte[] bytes = [UNIT_ID, 0x05, 0x01, 0x0A, 0xFF, 0x00, 0x00, 0x00];
+ var protocol = new RtuProtocol();
+
+ // Act
+ var coil = protocol.DeserializeWriteSingleCoil(bytes);
+
+ // Assert
+ Assert.IsNotNull(coil);
+ Assert.AreEqual(266, coil.Address);
+ Assert.IsTrue(coil.Value);
+ }
+
+ #endregion Write Single Coil
+
+ #region Write Single Register
+
+ [TestMethod]
+ public void ShouldSerializeWriteSingleHoldingRegister()
+ {
+ // Arrange
+ var register = new HoldingRegister { Address = 109, Value = 123 };
+ var protocol = new RtuProtocol();
+
+ // Act
+ var result = protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, register);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual(8, result.Count);
+
+ // Unit id
+ Assert.AreEqual(UNIT_ID, result[0]);
+
+ // Function code
+ Assert.AreEqual(0x06, result[1]);
+
+ // Starting address
+ Assert.AreEqual(0x00, result[2]);
+ Assert.AreEqual(0x6D, result[3]);
+
+ // Value
+ Assert.AreEqual(0x00, result[4]);
+ Assert.AreEqual(0x7B, result[5]);
+
+ // CRC check will be ignored
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowArgumentOutOfRangeForUnitIdOnSerializeWriteSingleHoldingRegister()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeWriteSingleHoldingRegister(0x00, new HoldingRegister());
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public void ShouldThrowArgumentNullOnSerializeWriteSingleHoldingRegister()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, null);
+
+ // Assert - ArgumentNullException
+ }
+
+ [TestMethod]
+ public void ShouldDeserializeWriteSingleHoldingRegister()
+ {
+ // Arrange
+ byte[] bytes = [UNIT_ID, 0x06, 0x02, 0x02, 0x01, 0x23, 0x00, 0x00];
+ var protocol = new RtuProtocol();
+
+ // Act
+ var register = protocol.DeserializeWriteSingleHoldingRegister(bytes);
+
+ // Assert
+ Assert.IsNotNull(register);
+ Assert.AreEqual(514, register.Address);
+ Assert.AreEqual(291, register.Value);
+ }
+
+ #endregion Write Single Register
+
+ #region Write Multiple Coils
+
+ [TestMethod]
+ public void ShouldSerializeWriteMultipleCoils()
+ {
+ // Arrange
+ var coils = new Coil[]
+ {
+ new() { Address = 10, Value = true },
+ new() { Address = 11, Value = false },
+ new() { Address = 12, Value = true },
+ new() { Address = 13, Value = false },
+ new() { Address = 14, Value = true },
+ };
+ var protocol = new RtuProtocol();
+
+ // Act
+ var result = protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual(10, result.Count);
+
+ // Unit id
+ Assert.AreEqual(UNIT_ID, result[0]);
+
+ // Function code
+ Assert.AreEqual(0x0F, result[1]);
+
+ // Starting address
+ Assert.AreEqual(0x00, result[2]);
+ Assert.AreEqual(0x0A, result[3]);
+
+ // Quantity
+ Assert.AreEqual(0x00, result[4]);
+ Assert.AreEqual(0x05, result[5]);
+
+ // Byte count
+ Assert.AreEqual(0x01, result[6]);
+
+ // Values
+ Assert.AreEqual(0x15, result[7]);
+
+ // CRC check will be ignored
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowArgumentOutOfRangeForUnitIdOnSerializeWriteMultipleCoils()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeWriteMultipleCoils(0x00, new List());
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public void ShouldThrowArgumentNullOnSerializeWriteMultipleCoils()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeWriteMultipleCoils(UNIT_ID, null);
+
+ // Assert - ArgumentNullException
+ }
+
+ [DataTestMethod]
+ [DataRow(0)]
+ [DataRow(1969)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleCoils(int count)
+ {
+ // Arrange
+ var coils = new List();
+ for (int i = 0; i < count; i++)
+ coils.Add(new() { Address = (ushort)i });
+
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleCoils()
+ {
+ // Arrange
+ var coils = new Coil[]
+ {
+ new() { Address = 10, Value = true },
+ new() { Address = 10, Value = false },
+ };
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
+
+ // Assert - ArgumentException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleCoils()
+ {
+ // Arrange
+ var coils = new Coil[]
+ {
+ new() { Address = 10, Value = true },
+ new() { Address = 12, Value = false },
+ };
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
+
+ // Assert - ArgumentException
+ }
+
+ [TestMethod]
+ public void ShouldDeserializeWriteMultipleCoils()
+ {
+ // Arrange
+ byte[] bytes = [UNIT_ID, 0x0F, 0x01, 0x0A, 0x00, 0x0B, 0x00, 0x00];
+ var protocol = new RtuProtocol();
+
+ // Act
+ var (firstAddress, numberOfCoils) = protocol.DeserializeWriteMultipleCoils(bytes);
+
+ // Assert
+ Assert.AreEqual(266, firstAddress);
+ Assert.AreEqual(11, numberOfCoils);
+ }
+
+ #endregion Write Multiple Coils
+
+ #region Write Multiple Holding Registers
+
+ [TestMethod]
+ public void ShouldSerializeWriteMultipleHoldingRegisters()
+ {
+ // Arrange
+ var registers = new HoldingRegister[]
+ {
+ new() { Address = 10, Value = 10 },
+ new() { Address = 11, Value = 11 }
+ };
+ var protocol = new RtuProtocol();
+
+ // Act
+ var result = protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual(13, result.Count);
+
+ // Unit id
+ Assert.AreEqual(UNIT_ID, result[0]);
+
+ // Function code
+ Assert.AreEqual(0x10, result[1]);
+
+ // Starting address
+ Assert.AreEqual(0x00, result[2]);
+ Assert.AreEqual(0x0A, result[3]);
+
+ // Quantity
+ Assert.AreEqual(0x00, result[4]);
+ Assert.AreEqual(0x02, result[5]);
+
+ // Byte count
+ Assert.AreEqual(0x04, result[6]);
+
+ // Values
+ Assert.AreEqual(0x00, result[7]);
+ Assert.AreEqual(0x0A, result[8]);
+ Assert.AreEqual(0x00, result[9]);
+ Assert.AreEqual(0x0B, result[10]);
+
+ // CRC check will be ignored
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowArgumentOutOfRangeForUnitIdOnSerializeWriteMultipleHoldingRegisters()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeWriteMultipleHoldingRegisters(0x00, new List());
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public void ShouldThrowArgumentNullOnSerializeWriteMultipleHoldingRegisters()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, null);
+
+ // Assert - ArgumentNullException
+ }
+
+ [DataTestMethod]
+ [DataRow(0)]
+ [DataRow(124)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleHoldingRegisters(int count)
+ {
+ // Arrange
+ var registers = new List();
+ for (int i = 0; i < count; i++)
+ registers.Add(new() { Address = (ushort)i });
+
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleHoldingRegisters()
+ {
+ // Arrange
+ var registers = new HoldingRegister[]
+ {
+ new() { Address = 10 },
+ new() { Address = 10 },
+ };
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
+
+ // Assert - ArgumentException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleHoldingRegisters()
+ {
+ // Arrange
+ var registers = new HoldingRegister[]
+ {
+ new() { Address = 10 },
+ new() { Address = 12 },
+ };
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
+
+ // Assert - ArgumentException
+ }
+
+ [TestMethod]
+ public void ShouldDeserializeWriteMultipleHoldingRegisters()
+ {
+ // Arrange
+ byte[] bytes = [UNIT_ID, 0x10, 0x02, 0x0A, 0x00, 0x0A, 0x00, 0x00];
+ var protocol = new RtuProtocol();
+
+ // Act
+ var (firstAddress, numberOfCoils) = protocol.DeserializeWriteMultipleHoldingRegisters(bytes);
+
+ // Assert
+ Assert.AreEqual(522, firstAddress);
+ Assert.AreEqual(10, numberOfCoils);
+ }
+
+ #endregion Write Multiple Holding Registers
+
+ #region Validation
+
+ [TestMethod]
+ public void ShouldReturnFalseForMinLengthOnCheckResponseComplete()
+ {
+ // Arrange
+ byte[] bytes = [UNIT_ID, 0x01];
+ var protocol = new RtuProtocol();
+
+ // Act
+ bool complete = protocol.CheckResponseComplete(bytes);
+
+ // Assert
+ Assert.IsFalse(complete);
+ }
+
+ [TestMethod]
+ public void ShouldReturnFalseForErrorOnCheckResponseComplete()
+ {
+ // Arrange
+ byte[] bytes = [UNIT_ID, 0x81, 0x01, 0x00];
+ var protocol = new RtuProtocol();
+
+ // Act
+ bool complete = protocol.CheckResponseComplete(bytes);
+
+ // Assert
+ Assert.IsFalse(complete);
+ }
+
+ [TestMethod]
+ public void ShouldReturnTrueForErrorOnCheckResponseComplete()
+ {
+ // Arrange
+ byte[] bytes = [UNIT_ID, 0x81, 0x01, 0x00, 0x00];
+ var protocol = new RtuProtocol();
+
+ // Act
+ bool complete = protocol.CheckResponseComplete(bytes);
+
+ // Assert
+ Assert.IsTrue(complete);
+ }
+
+ [DataTestMethod]
+ [DataRow(0x01)] // Read Coils
+ [DataRow(0x02)] // Read Discrete Inputs
+ [DataRow(0x03)] // Read Holding Registers
+ [DataRow(0x04)] // Read Input Registers
+ public void ShouldReturnFalseForMissingBytesOnReadFunctionsOnCheckResponseComplete(int functionCode)
+ {
+ // Arrange
+ byte[] bytes = [UNIT_ID, (byte)functionCode, 0x01, 0x00, 0x00];
+ var protocol = new RtuProtocol();
+
+ // Act
+ bool complete = protocol.CheckResponseComplete(bytes);
+
+ // Assert
+ Assert.IsFalse(complete);
+ }
+
+ [DataTestMethod]
+ [DataRow(0x01)] // Read Coils
+ [DataRow(0x02)] // Read Discrete Inputs
+ [DataRow(0x03)] // Read Holding Registers
+ [DataRow(0x04)] // Read Input Registers
+ public void ShouldReturnTrueOnReadFunctionsOnCheckResponseComplete(int functionCode)
+ {
+ // Arrange
+ byte[] bytes = [UNIT_ID, (byte)functionCode, 0x01, 0x00, 0x12, 0x34];
+ var protocol = new RtuProtocol();
+
+ // Act
+ bool complete = protocol.CheckResponseComplete(bytes);
+
+ // Assert
+ Assert.IsTrue(complete);
+ }
+
+ [DataTestMethod]
+ [DataRow(0x05)] // Write Single Coil
+ [DataRow(0x06)] // Write Single Register
+ [DataRow(0x0F)] // Write Multiple Coils
+ [DataRow(0x10)] // Write Multiple Registers
+ public void ShouldReturnFalseForMissingBytesOnWriteFunctionsOnCheckResponseComplete(int functionCode)
+ {
+ // Arrange
+ byte[] bytes = [UNIT_ID, (byte)functionCode, 0x00, 0x10, 0xFF, 0x00, 0x00];
+ var protocol = new RtuProtocol();
+
+ // Act
+ bool complete = protocol.CheckResponseComplete(bytes);
+
+ // Assert
+ Assert.IsFalse(complete);
+ }
+
+ [DataTestMethod]
+ [DataRow(0x05)] // Write Single Coil
+ [DataRow(0x06)] // Write Single Register
+ [DataRow(0x0F)] // Write Multiple Coils
+ [DataRow(0x10)] // Write Multiple Registers
+ public void ShouldReturnTrueOnWriteFunctionsOnCheckResponseComplete(int functionCode)
+ {
+ // Arrange
+ byte[] bytes = [UNIT_ID, (byte)functionCode, 0x00, 0x10, 0xFF, 0x00, 0x12, 0x34];
+ var protocol = new RtuProtocol();
+
+ // Act
+ bool complete = protocol.CheckResponseComplete(bytes);
+
+ // Assert
+ Assert.IsTrue(complete);
+ }
+
+ [TestMethod]
+ public void ShouldReturnFalseForMissingBytesOnReadDeviceIdentificationOnCheckResponseComplete()
+ {
+ // Arrange
+ byte[] bytes = [UNIT_ID, 0x2B, 0x0E, 0x01, 0x81, 0x00, 0x00];
+ var protocol = new RtuProtocol();
+
+ // Act
+ bool complete = protocol.CheckResponseComplete(bytes);
+
+ // Assert
+ Assert.IsFalse(complete);
+ }
+
+ [TestMethod]
+ public void ShouldReturnFalseForMissingCrcOnReadDeviceIdentificationOnCheckResponseComplete()
+ {
+ // Arrange
+ byte[] bytes = [UNIT_ID, 0x2B, 0x0E, 0x01, 0x81, 0x00, 0x00, 0x00];
+ var protocol = new RtuProtocol();
+
+ // Act
+ bool complete = protocol.CheckResponseComplete(bytes);
+
+ // Assert
+ Assert.IsFalse(complete);
+ }
+
+ [TestMethod]
+ public void ShouldReturnTrueOnReadDeviceIdentificationForZeroObjectsOnCheckResponseComplete()
+ {
+ // Arrange
+ byte[] bytes = [UNIT_ID, 0x2B, 0x0E, 0x01, 0x81, 0x00, 0x00, 0x00, 0x12, 0x34];
+ var protocol = new RtuProtocol();
+
+ // Act
+ bool complete = protocol.CheckResponseComplete(bytes);
+
+ // Assert
+ Assert.IsTrue(complete);
+ }
+
+ [TestMethod]
+ public void ShouldReturnFalseOnMissingBytesForDeviceIdentificationOnCheckResponseComplete()
+ {
+ // Arrange
+ byte[] bytes = [UNIT_ID, 0x2B, 0x0E, 0x01, 0x81, 0x00, 0x00, 0x02, 0x00, 0x02, 0x55, 0x66];
+ var protocol = new RtuProtocol();
+
+ // Act
+ bool complete = protocol.CheckResponseComplete(bytes);
+
+ // Assert
+ Assert.IsFalse(complete);
+ }
+
+ [TestMethod]
+ public void ShouldReturnTrueForDeviceIdentificationOnCheckResponseComplete()
+ {
+ // Arrange
+ byte[] bytes = [UNIT_ID, 0x2B, 0x0E, 0x01, 0x81, 0x00, 0x00, 0x02, 0x00, 0x02, 0x55, 0x66, 0x01, 0x01, 0x77, 0x12, 0x34];
+ var protocol = new RtuProtocol();
+
+ // Act
+ bool complete = protocol.CheckResponseComplete(bytes);
+
+ // Assert
+ Assert.IsTrue(complete);
+ }
+
+ [TestMethod]
+ public void ShouldValidateReadResponse()
+ {
+ // Arrange
+ byte[] request = [UNIT_ID, 0x01, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK
+ byte[] response = [UNIT_ID, 0x01, 0x01, 0x00, 0x00, 0x00];
+ AddCrc(response);
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.ValidateResponse(request, response);
+ }
+
+ [TestMethod]
+ public void ShouldValidateWriteResponse()
+ {
+ // Arrange
+ byte[] request = [UNIT_ID, 0x05, 0x00, 0x01, 0xFF, 0x00]; // CRC missing, OK
+ byte[] response = [UNIT_ID, 0x05, 0x00, 0x01, 0xFF, 0x00, 0x00, 0x00];
+ AddCrc(response);
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.ValidateResponse(request, response);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowForUnitIdOnValidateResponse()
+ {
+ // Arrange
+ byte[] request = [UNIT_ID, 0x01, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK
+ byte[] response = [UNIT_ID + 1, 0x01, 0x01, 0x00, 0x00, 0x00];
+ AddCrc(response);
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.ValidateResponse(request, response);
+ }
+
+ [DataTestMethod]
+ [DataRow(0x57, 0x6C)]
+ [DataRow(0x58, 0x6B)]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowForCrcOnValidateResponse(int hi, int lo)
+ {
+ // Arrange
+ byte[] request = [UNIT_ID, 0x01, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK
+ byte[] response = [UNIT_ID, 0x01, 0x01, 0x00, (byte)hi, (byte)lo];
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.ValidateResponse(request, response);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowForFunctionCodeOnValidateResponse()
+ {
+ // Arrange
+ byte[] request = [UNIT_ID, 0x01, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK
+ byte[] response = [UNIT_ID, 0x02, 0x01, 0x00, 0x00, 0x00];
+ AddCrc(response);
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.ValidateResponse(request, response);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowForErrorOnValidateResponse()
+ {
+ // Arrange
+ byte[] request = [UNIT_ID, 0x01, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK
+ byte[] response = [UNIT_ID, 0x81, 0x01, 0x00, 0x00];
+ AddCrc(response);
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.ValidateResponse(request, response);
+ }
+
+ [DataTestMethod]
+ [DataRow(0x01)]
+ [DataRow(0x02)]
+ [DataRow(0x03)]
+ [DataRow(0x04)]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowForReadLengthOnValidateResponse(int fn)
+ {
+ // Arrange
+ byte[] request = [UNIT_ID, (byte)fn, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK
+ byte[] response = [UNIT_ID, (byte)fn, 0xFF, 0x00, 0x00, 0x00, 0x00];
+ AddCrc(response);
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.ValidateResponse(request, response);
+ }
+
+ [DataTestMethod]
+ [DataRow(0x05)]
+ [DataRow(0x06)]
+ [DataRow(0x0F)]
+ [DataRow(0x10)]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowForWriteLengthOnValidateResponse(int fn)
+ {
+ // Arrange
+ byte[] request = [UNIT_ID, (byte)fn, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK
+ byte[] response = [UNIT_ID, (byte)fn, 0x00, 0x13, 0x00, 0x02, 0x00, 0x00, 0x00];
+ AddCrc(response);
+ var protocol = new RtuProtocol();
+
+ // Act
+ protocol.ValidateResponse(request, response);
+ }
+
+ [TestMethod]
+ public void ShouldReturnValidCrc16()
+ {
+ // This is the example of the spec, page 41.
+
+ // Arrange
+ byte[] bytes = [0x02, 0x07];
+
+ // Act
+ byte[] crc = RtuProtocol.CRC16(bytes);
+
+ // Assert
+ Assert.AreEqual(2, crc.Length);
+ Assert.AreEqual(0x41, crc[0]);
+ Assert.AreEqual(0x12, crc[1]);
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow(new byte[0])]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public void ShuldThrowArgumentNullExceptionForBytesOnCrc16(byte[] bytes)
+ {
+ // Act
+ _ = RtuProtocol.CRC16(bytes);
+
+ // Assert - ArgumentNullException
+ }
+
+ [DataTestMethod]
+ [DataRow(-1)]
+ [DataRow(10)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowArgumentOutOfRangeForStartOnCrc16(int start)
+ {
+ // Arrange
+ byte[] bytes = Encoding.UTF8.GetBytes("0123456789");
+
+ // Act
+ _ = RtuProtocol.CRC16(bytes, start);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [DataTestMethod]
+ [DataRow(0)]
+ [DataRow(11)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowArgumentOutOfRangeForLengthOnCrc16(int length)
+ {
+ // Arrange
+ byte[] bytes = Encoding.UTF8.GetBytes("0123456789");
+
+ // Act
+ _ = RtuProtocol.CRC16(bytes, 0, length);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ #endregion Validation
+
+ [TestMethod]
+ public void ShouldNameRtu()
+ {
+ // Arrange
+ var protocol = new RtuProtocol();
+
+ // Act
+ string result = protocol.Name;
+
+ // Assert
+ Assert.AreEqual("RTU", result);
+ }
+
+ private static void AddCrc(byte[] bytes)
+ {
+ byte[] crc = RtuProtocol.CRC16(bytes, 0, bytes.Length - 2);
+ bytes[^2] = crc[0];
+ bytes[^1] = crc[1];
+ }
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Tests/Common/Protocols/TcpProtocolTest.cs b/AMWD.Protocols.Modbus.Tests/Common/Protocols/TcpProtocolTest.cs
index 4c8d7b9..b2bade7 100644
--- a/AMWD.Protocols.Modbus.Tests/Common/Protocols/TcpProtocolTest.cs
+++ b/AMWD.Protocols.Modbus.Tests/Common/Protocols/TcpProtocolTest.cs
@@ -484,7 +484,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
- public void ShouldTrhowOutOfRangeExceptionOnSerializeReadDeviceIdentification()
+ public void ShouldThrowOutOfRangeExceptionOnSerializeReadDeviceIdentification()
{
// Arrange
var protocol = new TcpProtocol();
@@ -750,7 +750,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
- public void ShouldTrhowArgumentNullOnSerializeWriteMultipleCoils()
+ public void ShouldThrowArgumentNullOnSerializeWriteMultipleCoils()
{
// Arrange
var protocol = new TcpProtocol();
@@ -891,7 +891,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
- public void ShouldTrhowArgumentNullOnSerializeWriteMultipleHoldingRegisters()
+ public void ShouldThrowArgumentNullOnSerializeWriteMultipleHoldingRegisters()
{
// Arrange
var protocol = new TcpProtocol();