From dee0d674530a0f21617cdffa148cc182450a4e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Wed, 27 Mar 2024 20:39:43 +0100 Subject: [PATCH] Implemented the RTU protocol --- .../Protocols/RtuProtocol.cs | 792 ++++++++++ .../Common/Protocols/RtuProtocolTest.cs | 1390 +++++++++++++++++ .../Common/Protocols/TcpProtocolTest.cs | 6 +- 3 files changed, 2185 insertions(+), 3 deletions(-) create mode 100644 AMWD.Protocols.Modbus.Common/Protocols/RtuProtocol.cs create mode 100644 AMWD.Protocols.Modbus.Tests/Common/Protocols/RtuProtocolTest.cs 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();