diff --git a/AMWD.Protocols.Modbus.Common/Protocols/AsciiProtocol.cs b/AMWD.Protocols.Modbus.Common/Protocols/AsciiProtocol.cs new file mode 100644 index 0000000..5fbcba4 --- /dev/null +++ b/AMWD.Protocols.Modbus.Common/Protocols/AsciiProtocol.cs @@ -0,0 +1,733 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using AMWD.Protocols.Modbus.Common.Contracts; + +namespace AMWD.Protocols.Modbus.Common.Protocols +{ + /// + /// Default implementation of the Modbus ASCII protocol. + /// + public class AsciiProtocol : IModbusProtocol + { + #region Constants + + /// + /// The minimum allowed unit id specified by the Modbus SerialLine protocol. + /// + /// + /// INFORMATION: + ///
+ /// Reading the specification, the minimum allowed unit ID would be 1. + ///
+ /// As of other implementations seen, this limit is not enforced! + ///
+ public const byte MIN_UNIT_ID = 0x01; + + /// + /// The maximum allowed unit id specified by the Modbus SerialLine 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 SerialLine protocol. + /// + public const ushort MIN_READ_COUNT = 0x01; + + /// + /// The minimum allowed write count specified by the Modbus SerialLine protocol. + /// + public const ushort MIN_WRITE_COUNT = 0x01; + + /// + /// The maximum allowed read count for discrete values specified by the Modbus SerialLine protocol. + /// + public const ushort MAX_DISCRETE_READ_COUNT = 0x07D0; // 2000 + + /// + /// The maximum allowed write count for discrete values specified by the Modbus SerialLine protocol. + /// + public const ushort MAX_DISCRETE_WRITE_COUNT = 0x07B0; // 1968 + + /// + /// The maximum allowed read count for registers specified by the Modbus SerialLine protocol. + /// + public const ushort MAX_REGISTER_READ_COUNT = 0x007D; // 125 + + /// + /// The maximum allowed write count for registers specified by the Modbus SerialLine protocol. + /// + public const ushort MAX_REGISTER_WRITE_COUNT = 0x007B; // 123 + + /// + /// The maximum allowed ADU length in chars. + /// + /// + /// 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 = 513; // chars in ASCII (so bytes in the end) + + #endregion Constants + + /// + public string Name => "ASCII"; + + #region Read + + /// + public IReadOnlyList SerializeReadCoils(byte unitId, ushort startAddress, ushort count) + { + 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}"); + + // Unit Id and Function code + string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadCoils:X2}"; + + // Starting address + byte[] addrBytes = startAddress.ToBigEndianBytes(); + request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; + + // Quantity + byte[] countBytes = count.ToBigEndianBytes(); + request += $"{countBytes[0]:X2}{countBytes[1]:X2}"; + + // LRC + string lrc = LRC(request); + request += lrc; + + // CRLF + request += "\r\n"; + + return Encoding.ASCII.GetBytes(request); + } + + /// + public IReadOnlyList DeserializeReadCoils(IReadOnlyList response) + { + string responseMessage = Encoding.ASCII.GetString([.. response]).ToUpper(); + + byte numBytes = HexToByte(responseMessage.Substring(5, 2)); + byte[] responsePayloadBytes = HexStringToByteArray(responseMessage.Substring(7, responseMessage.Length - 11)); + + if (numBytes != responsePayloadBytes.Length) + throw new ModbusException("Coil byte count does not match."); + + int count = numBytes * 8; + var coils = new List(); + for (int i = 0; i < count; i++) + { + int bytePosition = i / 8; + int bitPosition = i % 8; + + int value = responsePayloadBytes[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 (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}"); + + // Unit Id and Function code + string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadDiscreteInputs:X2}"; + + // Starting address + byte[] addrBytes = startAddress.ToBigEndianBytes(); + request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; + + // Quantity + byte[] countBytes = count.ToBigEndianBytes(); + request += $"{countBytes[0]:X2}{countBytes[1]:X2}"; + + // LRC + string lrc = LRC(request); + request += lrc; + + // CRLF + request += "\r\n"; + + return Encoding.ASCII.GetBytes(request); + } + + /// + public IReadOnlyList DeserializeReadDiscreteInputs(IReadOnlyList response) + { + string responseMessage = Encoding.ASCII.GetString([.. response]).ToUpper(); + + byte numBytes = HexToByte(responseMessage.Substring(5, 2)); + byte[] responsePayloadBytes = HexStringToByteArray(responseMessage.Substring(7, responseMessage.Length - 11)); + + if (numBytes != responsePayloadBytes.Length) + throw new ModbusException("Discrete input byte count does not match."); + + int count = numBytes * 8; + var discreteInputs = new List(); + for (int i = 0; i < count; i++) + { + int bytePosition = i / 8; + int bitPosition = i % 8; + + int value = responsePayloadBytes[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 (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}"); + + string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadHoldingRegisters:X2}"; + + // Starting address + byte[] addrBytes = startAddress.ToBigEndianBytes(); + request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; + + // Quantity + byte[] countBytes = count.ToBigEndianBytes(); + request += $"{countBytes[0]:X2}{countBytes[1]:X2}"; + + // LRC + string lrc = LRC(request); + request += lrc; + + // CRLF + request += "\r\n"; + + return Encoding.ASCII.GetBytes(request); + } + + /// + public IReadOnlyList DeserializeReadHoldingRegisters(IReadOnlyList response) + { + string responseMessage = Encoding.ASCII.GetString([.. response]).ToUpper(); + + byte numBytes = HexToByte(responseMessage.Substring(5, 2)); + byte[] responsePayloadBytes = HexStringToByteArray(responseMessage.Substring(7, responseMessage.Length - 11)); + + if (numBytes != responsePayloadBytes.Length) + throw new ModbusException("Holding register byte count does not match."); + + int count = numBytes / 2; + var holdingRegisters = new List(); + for (int i = 0; i < count; i++) + { + holdingRegisters.Add(new HoldingRegister + { + Address = (ushort)i, + HighByte = responsePayloadBytes[i * 2], + LowByte = responsePayloadBytes[i * 2 + 1] + }); + } + + return holdingRegisters; + } + + /// + public IReadOnlyList SerializeReadInputRegisters(byte unitId, ushort startAddress, ushort count) + { + 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}"); + + string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadInputRegisters:X2}"; + + // Starting address + byte[] addrBytes = startAddress.ToBigEndianBytes(); + request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; + + // Quantity + byte[] countBytes = count.ToBigEndianBytes(); + request += $"{countBytes[0]:X2}{countBytes[1]:X2}"; + + // LRC + string lrc = LRC(request); + request += lrc; + + // CRLF + request += "\r\n"; + + return Encoding.ASCII.GetBytes(request); + } + + /// + public IReadOnlyList DeserializeReadInputRegisters(IReadOnlyList response) + { + string responseMessage = Encoding.ASCII.GetString([.. response]).ToUpper(); + + byte numBytes = HexToByte(responseMessage.Substring(5, 2)); + byte[] responsePayloadBytes = HexStringToByteArray(responseMessage.Substring(7, responseMessage.Length - 11)); + + if (numBytes != responsePayloadBytes.Length) + throw new ModbusException("Input register byte count does not match."); + + int count = numBytes / 2; + var inputRegisters = new List(); + for (int i = 0; i < count; i++) + { + inputRegisters.Add(new InputRegister + { + Address = (ushort)i, + HighByte = responsePayloadBytes[i * 2], + LowByte = responsePayloadBytes[i * 2 + 1] + }); + } + + return inputRegisters; + } + + /// + public IReadOnlyList SerializeReadDeviceIdentification(byte unitId, ModbusDeviceIdentificationCategory category, ModbusDeviceIdentificationObject objectId) + { + if (!Enum.IsDefined(typeof(ModbusDeviceIdentificationCategory), category)) + throw new ArgumentOutOfRangeException(nameof(category)); + + // Unit Id, Function code and Modbus Encapsulated Interface: Read Device Identification (MEI Type) + string request = $":{unitId:X2}{(byte)ModbusFunctionCode.EncapsulatedInterface:X2}0E"; + + // The category type (basic, regular, extended, individual) + request += $"{(byte)category:X2}{(byte)objectId:X2}"; + + // LRC + string lrc = LRC(request); + request += lrc; + + // CRLF + request += "\r\n"; + + return Encoding.ASCII.GetBytes(request); + } + + /// + public DeviceIdentificationRaw DeserializeReadDeviceIdentification(IReadOnlyList response) + { + string responseMessage = Encoding.ASCII.GetString([.. response]).ToUpper(); + + if (responseMessage.Substring(5, 2) != "0E") + throw new ModbusException("The MEI type does not match"); + + byte category = HexToByte(responseMessage.Substring(7, 2)); + if (!Enum.IsDefined(typeof(ModbusDeviceIdentificationCategory), category)) + throw new ModbusException("The category type does not match"); + + var deviceIdentification = new DeviceIdentificationRaw + { + AllowsIndividualAccess = (HexToByte(responseMessage.Substring(9, 2)) & 0x80) == 0x80, + MoreRequestsNeeded = responseMessage.Substring(11, 2) == "FF", + NextObjectIdToRequest = HexToByte(responseMessage.Substring(13, 2)), + }; + + byte[] responsePayloadBytes = HexStringToByteArray(responseMessage.Substring(15, responseMessage.Length - 19)); + + int baseOffset = 1; // Skip number of objects + while (baseOffset < responsePayloadBytes.Length) + { + byte objectId = responsePayloadBytes[baseOffset]; + byte length = responsePayloadBytes[baseOffset + 1]; + + byte[] data = responsePayloadBytes.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 NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(coil); +#else + if (coil == null) + throw new ArgumentNullException(nameof(coil)); +#endif + + // Unit Id and Function code + string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteSingleCoil:X2}"; + + // Starting address + byte[] addrBytes = coil.Address.ToBigEndianBytes(); + request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; + + // Value + request += $"{coil.HighByte:X2}{coil.LowByte:X2}"; + + // LRC + string lrc = LRC(request); + request += lrc; + + // CRLF + request += "\r\n"; + + return Encoding.ASCII.GetBytes(request); + } + + /// + public Coil DeserializeWriteSingleCoil(IReadOnlyList response) + { + string responseMessage = Encoding.ASCII.GetString([.. response]).ToUpper(); + + return new Coil + { + Address = HexStringToByteArray(responseMessage.Substring(5, 4)).GetBigEndianUInt16(), + HighByte = HexToByte(responseMessage.Substring(9, 2)), + LowByte = HexToByte(responseMessage.Substring(11, 2)) + }; + } + + /// + public IReadOnlyList SerializeWriteSingleHoldingRegister(byte unitId, HoldingRegister register) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(register); +#else + if (register == null) + throw new ArgumentNullException(nameof(register)); +#endif + + // Unit Id and Function code + string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteSingleRegister:X2}"; + + // Starting address + byte[] addrBytes = register.Address.ToBigEndianBytes(); + request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; + + // Value + request += $"{register.HighByte:X2}{register.LowByte:X2}"; + + // LRC + string lrc = LRC(request); + request += lrc; + + // CRLF + request += "\r\n"; + + return Encoding.ASCII.GetBytes(request); + } + + /// + public HoldingRegister DeserializeWriteSingleHoldingRegister(IReadOnlyList response) + { + string responseMessage = Encoding.ASCII.GetString([.. response]).ToUpper(); + + return new HoldingRegister + { + Address = HexStringToByteArray(responseMessage.Substring(5, 4)).GetBigEndianUInt16(), + HighByte = HexToByte(responseMessage.Substring(9, 2)), + LowByte = HexToByte(responseMessage.Substring(11, 2)) + }; + } + + /// + public IReadOnlyList SerializeWriteMultipleCoils(byte unitId, IReadOnlyList coils) + { +#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[] data = new byte[byteCount]; + 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); + data[bytePosition] |= bitMask; + } + } + + // Unit Id and Function code + string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteMultipleCoils:X2}"; + + // Starting address + byte[] addrBytes = firstAddress.ToBigEndianBytes(); + request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; + + // Quantity + byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); + request += $"{countBytes[0]:X2}{countBytes[1]:X2}"; + + // Byte count + request += $"{byteCount:X2}"; + + // Data + request += string.Join("", data.Select(b => $"{b:X2}")); + + // LRC + string lrc = LRC(request); + request += lrc; + + // CRLF + request += "\r\n"; + + return Encoding.ASCII.GetBytes(request); + } + + /// + public (ushort FirstAddress, ushort NumberOfCoils) DeserializeWriteMultipleCoils(IReadOnlyList response) + { + string responseMessage = Encoding.ASCII.GetString([.. response]).ToUpper(); + + ushort firstAddress = HexStringToByteArray(responseMessage.Substring(5, 4)).GetBigEndianUInt16(); + ushort numberOfCoils = HexStringToByteArray(responseMessage.Substring(9, 4)).GetBigEndianUInt16(); + + return (firstAddress, numberOfCoils); + } + + /// + public IReadOnlyList SerializeWriteMultipleHoldingRegisters(byte unitId, IReadOnlyList registers) + { +#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[] data = new byte[byteCount]; + for (int i = 0; i < orderedList.Count; i++) + { + data[2 * i] = orderedList[i].HighByte; + data[2 * i + 1] = orderedList[i].LowByte; + } + + // Unit Id and Function code + string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteMultipleRegisters:X2}"; + + // Starting address + byte[] addrBytes = firstAddress.ToBigEndianBytes(); + request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; + + // Quantity + byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); + request += $"{countBytes[0]:X2}{countBytes[1]:X2}"; + + // Byte count + request += $"{byteCount:X2}"; + + // Data + request += string.Join("", data.Select(b => $"{b:X2}")); + + // LRC + string lrc = LRC(request); + request += lrc; + + // CRLF + request += "\r\n"; + + return Encoding.ASCII.GetBytes(request); + } + + /// + public (ushort FirstAddress, ushort NumberOfRegisters) DeserializeWriteMultipleHoldingRegisters(IReadOnlyList response) + { + string responseMessage = Encoding.ASCII.GetString([.. response]).ToUpper(); + + ushort firstAddress = HexStringToByteArray(responseMessage.Substring(5, 4)).GetBigEndianUInt16(); + ushort numberOfRegisters = HexStringToByteArray(responseMessage.Substring(9, 4)).GetBigEndianUInt16(); + + return (firstAddress, numberOfRegisters); + } + + #endregion Write + + #region Validation + + /// + public bool CheckResponseComplete(IReadOnlyList responseBytes) + { + if (responseBytes.Count < 3) + return false; + + for (int i = responseBytes.Count - 2; i >= 0; i--) + { + // ASCII terminates with CR LF (\r\n) + if (responseBytes[i] == 0x0D && responseBytes[i + 1] == 0x0A) + return true; + } + + return false; + } + + /// + public void ValidateResponse(IReadOnlyList request, IReadOnlyList response) + { + string requestMessage = Encoding.ASCII.GetString([.. request]).ToUpper(); + string responseMessage = Encoding.ASCII.GetString([.. response]).ToUpper(); + + // Check header + if (!responseMessage.StartsWith(":")) + throw new ModbusException("The protocol header is missing."); + + // Check trailer + if (!responseMessage.EndsWith("\r\n")) + throw new ModbusException("The protocol tail is missing."); + + string calculatedLrc = LRC(responseMessage, 1, responseMessage.Length - 5); + string receivedLrc = responseMessage.Substring(responseMessage.Length - 4, 2); + if (calculatedLrc != receivedLrc) + throw new ModbusException("LRC check failed."); + + if (requestMessage.Substring(1, 2) != responseMessage.Substring(1, 2)) + throw new ModbusException("Unit Identifier does not match."); + + byte fnCode = HexToByte(responseMessage.Substring(3, 2)); + bool isError = (fnCode & 0x80) == 0x80; + if (isError) + fnCode = (byte)(fnCode ^ 0x80); // === fnCode & 0x7F + + if (requestMessage.Substring(3, 2) != fnCode.ToString("X2")) + throw new ModbusException("Function code does not match."); + + if (isError) + throw new ModbusException("Remote Error") { ErrorCode = (ModbusErrorCode)HexToByte(responseMessage.Substring(5, 2)) }; + + if (new[] { 0x01, 0x02, 0x03, 0x04 }.Contains(fnCode)) + { + // : ID FN NU DA XX \r\n + byte charByteCount = HexToByte(responseMessage.Substring(5, 2)); + if (responseMessage.Length != charByteCount * 2 + 11) + throw new ModbusException("Number of following bytes does not match."); + } + + if (new[] { 0x05, 0x06, 0x0F, 0x10 }.Contains(fnCode)) + { + // : ID FN 00 10 00 30 XX \r\n + if (responseMessage.Length != 17) + throw new ModbusException("Number of bytes does not match."); + } + + // TODO: Do we want to check 0x2B too? + } + + /// + /// Calculate LRC for Modbus ASCII. + /// + /// The message chars. + /// The start index. + /// The number of bytes to calculate. + public static string LRC(string message, int start = 1, int? length = null) + { + if (string.IsNullOrWhiteSpace(message)) + throw new ArgumentNullException(nameof(message)); + + if (start < 0 || start >= message.Length) + throw new ArgumentOutOfRangeException(nameof(start)); + + length ??= message.Length - start; + + if (length <= 0 || start + length > message.Length) + throw new ArgumentOutOfRangeException(nameof(length)); + + if (length % 2 != 0) + throw new ArgumentException("The number of chars to calculate the LRC must be even.", nameof(length)); + + string subStr = message.Substring(start, length.Value); + + // Step 1: + // Add all bytes in the message, excluding the starting 'colon' and ending CRLF. + // Add them into an 8–bit field, so that carries will be discarded. + byte lrc = 0x00; + foreach (byte b in HexStringToByteArray(subStr)) + lrc += b; + + // Step 2: + // Subtract the final field value from FF hex (all 1's), to produce the ones-complement. + byte oneComplement = (byte)(lrc ^ 0xFF); + + // Step 3: + // Add 1 to produce the twos-complement. + return ((byte)(oneComplement + 0x01)).ToString("X2"); + } + + #endregion Validation + + #region Private Helper + + private static byte[] HexStringToByteArray(string hexString) + { + return Enumerable + .Range(0, hexString.Length) + .Where(x => x % 2 == 0) + .Select(x => HexToByte(hexString.Substring(x, 2))) + .ToArray(); + } + + private static byte HexToByte(string hex) + => Convert.ToByte(hex, 16); + + #endregion Private Helper + } +} diff --git a/AMWD.Protocols.Modbus.Common/Protocols/RtuProtocol.cs b/AMWD.Protocols.Modbus.Common/Protocols/RtuProtocol.cs index e7c503c..26bf868 100644 --- a/AMWD.Protocols.Modbus.Common/Protocols/RtuProtocol.cs +++ b/AMWD.Protocols.Modbus.Common/Protocols/RtuProtocol.cs @@ -13,12 +13,19 @@ namespace AMWD.Protocols.Modbus.Common.Protocols #region Constants /// - /// The minimum allowed unit id specified by the Modbus TCP protocol. + /// The minimum allowed unit id specified by the Modbus SerialLine protocol. /// + /// + /// INFORMATION: + ///
+ /// Reading the specification, the minimum allowed unit ID would be 1. + ///
+ /// As of other implementations seen, this limit is not enforced! + ///
public const byte MIN_UNIT_ID = 0x01; /// - /// The maximum allowed unit id specified by the Modbus TCP protocol. + /// The maximum allowed unit id specified by the Modbus SerialLine protocol. /// /// /// Reading the specification, the max allowed unit id would be 247! @@ -26,32 +33,32 @@ namespace AMWD.Protocols.Modbus.Common.Protocols public const byte MAX_UNIT_ID = 0xFF; /// - /// The minimum allowed read count specified by the Modbus TCP protocol. + /// The minimum allowed read count specified by the Modbus SerialLine protocol. /// public const ushort MIN_READ_COUNT = 0x01; /// - /// The minimum allowed write count specified by the Modbus TCP protocol. + /// The minimum allowed write count specified by the Modbus SerialLine protocol. /// public const ushort MIN_WRITE_COUNT = 0x01; /// - /// The maximum allowed read count for discrete values specified by the Modbus TCP protocol. + /// The maximum allowed read count for discrete values specified by the Modbus SerialLine protocol. /// public const ushort MAX_DISCRETE_READ_COUNT = 0x07D0; // 2000 /// - /// The maximum allowed write count for discrete values specified by the Modbus TCP protocol. + /// The maximum allowed write count for discrete values specified by the Modbus SerialLine protocol. /// public const ushort MAX_DISCRETE_WRITE_COUNT = 0x07B0; // 1968 /// - /// The maximum allowed read count for registers specified by the Modbus TCP protocol. + /// The maximum allowed read count for registers specified by the Modbus SerialLine protocol. /// public const ushort MAX_REGISTER_READ_COUNT = 0x007D; // 125 /// - /// The maximum allowed write count for registers specified by the Modbus TCP protocol. + /// The maximum allowed write count for registers specified by the Modbus SerialLine protocol. /// public const ushort MAX_REGISTER_WRITE_COUNT = 0x007B; // 123 @@ -74,9 +81,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols /// 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)); @@ -137,9 +141,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols /// 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)); @@ -200,9 +201,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols /// 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)); @@ -260,9 +258,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols /// 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)); @@ -320,9 +315,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols /// 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)); @@ -387,9 +379,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols /// 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 @@ -434,9 +423,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols /// 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 @@ -481,9 +467,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols /// 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 @@ -555,9 +538,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols /// 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 diff --git a/AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs b/AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs index e5d52d5..4cd1d5c 100644 --- a/AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs +++ b/AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs @@ -91,9 +91,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols byte[] request = new byte[12]; - byte[] header = GetHeader(unitId, 6); + byte[] header = GetHeader(6); Array.Copy(header, 0, request, 0, header.Length); + // Unit id + request[6] = unitId; + // Function code request[7] = (byte)ModbusFunctionCode.ReadCoils; @@ -146,9 +149,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols byte[] request = new byte[12]; - byte[] header = GetHeader(unitId, 6); + byte[] header = GetHeader(6); Array.Copy(header, 0, request, 0, header.Length); + // Unit id + request[6] = unitId; + // Function code request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs; @@ -201,9 +207,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols byte[] request = new byte[12]; - byte[] header = GetHeader(unitId, 6); + byte[] header = GetHeader(6); Array.Copy(header, 0, request, 0, header.Length); + // Unit id + request[6] = unitId; + // Function code request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters; @@ -253,9 +262,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols byte[] request = new byte[12]; - byte[] header = GetHeader(unitId, 6); + byte[] header = GetHeader(6); Array.Copy(header, 0, request, 0, header.Length); + // Unit id + request[6] = unitId; + // Function code request[7] = (byte)ModbusFunctionCode.ReadInputRegisters; @@ -302,9 +314,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols byte[] request = new byte[11]; - byte[] header = GetHeader(unitId, 5); + byte[] header = GetHeader(5); Array.Copy(header, 0, request, 0, header.Length); + // Unit id + request[6] = unitId; + // Function code request[7] = (byte)ModbusFunctionCode.EncapsulatedInterface; @@ -365,9 +380,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols byte[] request = new byte[12]; - byte[] header = GetHeader(unitId, 6); + byte[] header = GetHeader(6); Array.Copy(header, 0, request, 0, header.Length); + // Unit id + request[6] = unitId; + // Function code request[7] = (byte)ModbusFunctionCode.WriteSingleCoil; @@ -404,9 +422,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols byte[] request = new byte[12]; - byte[] header = GetHeader(unitId, 6); + byte[] header = GetHeader(6); Array.Copy(header, 0, request, 0, header.Length); + // Unit id + request[6] = unitId; + // Function code request[7] = (byte)ModbusFunctionCode.WriteSingleRegister; @@ -458,21 +479,29 @@ namespace AMWD.Protocols.Modbus.Common.Protocols byte byteCount = (byte)Math.Ceiling(orderedList.Count / 8.0); byte[] request = new byte[13 + byteCount]; - byte[] header = GetHeader(unitId, byteCount + 7); + byte[] header = GetHeader(byteCount + 7); Array.Copy(header, 0, request, 0, header.Length); + // Unit id + request[6] = unitId; + + // Function code request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils; + // Starting address byte[] addrBytes = firstAddress.ToBigEndianBytes(); request[8] = addrBytes[0]; request[9] = addrBytes[1]; + // Quantity byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); request[10] = countBytes[0]; request[11] = countBytes[1]; + // Byte count request[12] = byteCount; + // Coils int baseOffset = 13; for (int i = 0; i < orderedList.Count; i++) { @@ -525,21 +554,29 @@ namespace AMWD.Protocols.Modbus.Common.Protocols byte byteCount = (byte)(orderedList.Count * 2); byte[] request = new byte[13 + byteCount]; - byte[] header = GetHeader(unitId, byteCount + 7); + byte[] header = GetHeader(byteCount + 7); Array.Copy(header, 0, request, 0, header.Length); + // Unit id + request[6] = unitId; + + // Function code request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters; + // Starting address byte[] addrBytes = firstAddress.ToBigEndianBytes(); request[8] = addrBytes[0]; request[9] = addrBytes[1]; + // Quantity byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); request[10] = countBytes[0]; request[11] = countBytes[1]; + // Byte count request[12] = byteCount; + // Registers int baseOffset = 13; for (int i = 0; i < orderedList.Count; i++) { @@ -633,15 +670,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols /// /// Generates the header for a Modbus request. /// - /// The unit identifier. /// The number of following bytes. - /// The header ready to copy to the request bytes. - /// - /// ATTENTION: Do not forget the . It is placed after the count information. - /// - private byte[] GetHeader(byte unitId, int followingBytes) + /// The header ready to copy to the request bytes (6 bytes). + private byte[] GetHeader(int followingBytes) { - byte[] header = new byte[7]; + byte[] header = new byte[6]; // Transaction id ushort txId = GetNextTransacitonId(); @@ -658,9 +691,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols header[4] = countBytes[0]; header[5] = countBytes[1]; - // Unit identifier - header[6] = unitId; - return header; } diff --git a/AMWD.Protocols.Modbus.Common/README.md b/AMWD.Protocols.Modbus.Common/README.md index 0c4d662..35c7aff 100644 --- a/AMWD.Protocols.Modbus.Common/README.md +++ b/AMWD.Protocols.Modbus.Common/README.md @@ -13,13 +13,19 @@ If you want to speak a custom type of protocol with the clients, you can impleme **ModbusBaseClient** This abstract base client contains all the basic methods and handlings required to communicate via Modbus Protocol. -The packages `AMWD.Protocols.Modbus.Serial` _(in progress)_ and `AMWD.Protocols.Modbus.Tcp` _(in progress)_ have specific derived implementations to match the communication types. +The packages `AMWD.Protocols.Modbus.Serial` _(in progress)_ and `AMWD.Protocols.Modbus.Tcp` have specific derived implementations to match the communication types. ### Enums Here you have all typed enumerables defined by the Modbus Protocol. +- Error code +- Function code +- Device Identification Category (Basic, Regular, Extended, Individual) +- Device Identification Object +- ModbusObjectType (only needed when using the abstract base type `ModbusObject` instead of `Coil`, etc.) + ### Extensions @@ -41,7 +47,7 @@ The different types handled by the Modbus Protocol. - Input Register In addition, you'll find the `DeviceIdentification` there. -It is used for a "special" function called "Read Device Identification" (0x2B / 43) not supported by all devices. +It is used for a "special" function called _Read Device Identification_ (0x2B / 43), not supported on all devices. The `ModbusDevice` is used for the server implementations in the derived packages. @@ -50,10 +56,14 @@ The `ModbusDevice` is used for the server implementations in the derived package Here you have the specific default implementations for the Modbus Protocol. -- ASCII _(in progress)_ -- RTU _(in progress)_ +- ASCII +- RTU +- RTU over TCP _(in progress)_ - TCP +**NOTE:** +The implementations over serial line (RTU and ASCII) have a minimum unit ID of one (1) referring to the specification. +This validation is _not_ implemented here due to real world experience, that some manufactures do not care about it. --- diff --git a/AMWD.Protocols.Modbus.Tests/Common/Protocols/AsciiProtocolTest.cs b/AMWD.Protocols.Modbus.Tests/Common/Protocols/AsciiProtocolTest.cs new file mode 100644 index 0000000..5a6f852 --- /dev/null +++ b/AMWD.Protocols.Modbus.Tests/Common/Protocols/AsciiProtocolTest.cs @@ -0,0 +1,1112 @@ +using System.Collections.Generic; +using System.Text; +using AMWD.Protocols.Modbus.Common.Protocols; + +namespace AMWD.Protocols.Modbus.Tests.Common.Protocols +{ + [TestClass] + public class AsciiProtocolTest + { + private const byte UNIT_ID = 0x2A; // 42 + + #region Read Coils + + [TestMethod] + public void ShouldSerializeReadCoils() + { + // Arrange + string expectedResponse = $":{UNIT_ID:X2}0100130013"; + AddTrailer(ref expectedResponse); + byte[] expectedBytes = Encoding.ASCII.GetBytes(expectedResponse); + + var protocol = new AsciiProtocol(); + + // Act + var bytes = protocol.SerializeReadCoils(UNIT_ID, 19, 19); + + // Assert + Assert.IsNotNull(bytes); + CollectionAssert.AreEqual(expectedBytes, bytes.ToArray()); + } + + [DataTestMethod] + [DataRow(0)] + [DataRow(2001)] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ShouldThrowOutOfRangeForCountOnSerializeReadCoils(int count) + { + // Arrange + var protocol = new AsciiProtocol(); + + // Act + protocol.SerializeReadCoils(UNIT_ID, 19, (ushort)count); + + // Assert - ArgumentOutOfRangeException + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadCoils() + { + // Arrange + var protocol = new AsciiProtocol(); + + // 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]; + + string response = $":{UNIT_ID:X2}0103CD6B05"; + AddTrailer(ref response); + byte[] responseBytes = Encoding.ASCII.GetBytes(response); + + var protocol = new AsciiProtocol(); + + // Act + var coils = protocol.DeserializeReadCoils(responseBytes); + + // 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 + string response = $":{UNIT_ID:X2}0102CD6B05"; + AddTrailer(ref response); + byte[] responseBytes = Encoding.ASCII.GetBytes(response); + + var protocol = new AsciiProtocol(); + + // Act + _ = protocol.DeserializeReadCoils(responseBytes); + + // Assert - ModbusException + } + + #endregion Read Coils + + #region Read Discrete Inputs + + [TestMethod] + public void ShouldSerializeReadDiscreteInputs() + { + // Arrange + string expectedResponse = $":{UNIT_ID:X2}0200130013"; + AddTrailer(ref expectedResponse); + byte[] expectedBytes = Encoding.ASCII.GetBytes(expectedResponse); + + var protocol = new AsciiProtocol(); + + // Act + var bytes = protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, 19); + + // Assert + Assert.IsNotNull(bytes); + CollectionAssert.AreEqual(expectedBytes, bytes.ToArray()); + } + + [DataTestMethod] + [DataRow(0)] + [DataRow(2001)] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ShouldThrowOutOfRangeForCountOnSerializeReadDiscreteInputs(int count) + { + // Arrange + var protocol = new AsciiProtocol(); + + // Act + protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, (ushort)count); + + // Assert - ArgumentOutOfRangeException + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadDiscreteInputs() + { + // Arrange + var protocol = new AsciiProtocol(); + + // 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]; + + string response = $":{UNIT_ID:X2}0203CD6B05"; + AddTrailer(ref response); + byte[] responseBytes = Encoding.ASCII.GetBytes(response); + + var protocol = new AsciiProtocol(); + + // Act + var discreteInputs = protocol.DeserializeReadDiscreteInputs(responseBytes); + + // Assert + Assert.IsNotNull(discreteInputs); + Assert.AreEqual(24, discreteInputs.Count); + + for (int i = 0; i < 24; i++) + { + Assert.AreEqual(i, discreteInputs[i].Address); + + if (setValues.Contains(i)) + Assert.IsTrue(discreteInputs[i].Value); + else + Assert.IsFalse(discreteInputs[i].Value); + } + } + + [TestMethod] + [ExpectedException(typeof(ModbusException))] + public void ShouldThrowExceptionOnDeserializeReadDiscreteInputs() + { + // Arrange + string response = $":{UNIT_ID:X2}0202CD6B05"; + AddTrailer(ref response); + byte[] responseBytes = Encoding.ASCII.GetBytes(response); + + var protocol = new AsciiProtocol(); + + // Act + protocol.DeserializeReadDiscreteInputs(responseBytes); + + // Assert - ModbusException + } + + #endregion Read Discrete Inputs + + #region Read Holding Registers + + [TestMethod] + public void ShouldSerializeReadHoldingRegisters() + { + // Arrange + string expectedResponse = $":{UNIT_ID:X2}0300130013"; + AddTrailer(ref expectedResponse); + byte[] expectedBytes = Encoding.ASCII.GetBytes(expectedResponse); + + var protocol = new AsciiProtocol(); + + // Act + var bytes = protocol.SerializeReadHoldingRegisters(UNIT_ID, 19, 19); + + // Assert + Assert.IsNotNull(bytes); + CollectionAssert.AreEqual(expectedBytes, bytes.ToArray()); + } + + [DataTestMethod] + [DataRow(0)] + [DataRow(2001)] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ShouldThrowOutOfRangeForCountOnSerializeReadHoldingRegisters(int count) + { + // Arrange + var protocol = new AsciiProtocol(); + + // Act + protocol.SerializeReadHoldingRegisters(UNIT_ID, 19, (ushort)count); + + // Assert - ArgumentOutOfRangeException + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadHoldingRegisters() + { + // Arrange + var protocol = new AsciiProtocol(); + + // Act + protocol.SerializeReadHoldingRegisters(UNIT_ID, ushort.MaxValue, 2); + + // Assert - ArgumentOutOfRangeException + } + + [TestMethod] + public void ShouldDeserializeReadHoldingRegisters() + { + // Arrange + string response = $":{UNIT_ID:X2}0304022B0064"; + AddTrailer(ref response); + byte[] responseBytes = Encoding.ASCII.GetBytes(response); + + var protocol = new AsciiProtocol(); + + // Act + var registers = protocol.DeserializeReadHoldingRegisters(responseBytes); + + // 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 + string response = $":{UNIT_ID:X2}0304022B"; + AddTrailer(ref response); + byte[] responseBytes = Encoding.ASCII.GetBytes(response); + + var protocol = new AsciiProtocol(); + + // Act + protocol.DeserializeReadHoldingRegisters(responseBytes); + + // Assert - ModbusException + } + + #endregion Read Holding Registers + + #region Read Input Registers + + [TestMethod] + public void ShouldSerializeReadInputRegisters() + { + // Arrange + string expectedResponse = $":{UNIT_ID:X2}0400130013"; + AddTrailer(ref expectedResponse); + byte[] expectedBytes = Encoding.ASCII.GetBytes(expectedResponse); + + var protocol = new AsciiProtocol(); + + // Act + var bytes = protocol.SerializeReadInputRegisters(UNIT_ID, 19, 19); + + // Assert + Assert.IsNotNull(bytes); + CollectionAssert.AreEqual(expectedBytes, bytes.ToArray()); + } + + [DataTestMethod] + [DataRow(0)] + [DataRow(2001)] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ShouldThrowOutOfRangeForCountOnSerializeReadInputRegisters(int count) + { + // Arrange + var protocol = new AsciiProtocol(); + + // Act + protocol.SerializeReadInputRegisters(UNIT_ID, 19, (ushort)count); + + // Assert - ArgumentOutOfRangeException + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadInputRegisters() + { + // Arrange + var protocol = new AsciiProtocol(); + + // Act + protocol.SerializeReadInputRegisters(UNIT_ID, ushort.MaxValue, 2); + + // Assert - ArgumentOutOfRangeException + } + + [TestMethod] + public void ShouldDeserializeReadInputRegisters() + { + // Arrange + string response = $":{UNIT_ID:X2}0404022B0064"; + AddTrailer(ref response); + byte[] responseBytes = Encoding.ASCII.GetBytes(response); + + var protocol = new AsciiProtocol(); + + // Act + var registers = protocol.DeserializeReadInputRegisters(responseBytes); + + // 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 + string response = $":{UNIT_ID:X2}0404022B"; + AddTrailer(ref response); + byte[] responseBytes = Encoding.ASCII.GetBytes(response); + + var protocol = new AsciiProtocol(); + + // Act + protocol.DeserializeReadInputRegisters(responseBytes); + + // 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 + string expectedResponse = $":{UNIT_ID:X2}2B0E{(byte)category:X2}{(byte)ModbusDeviceIdentificationObject.ProductCode:X2}"; + AddTrailer(ref expectedResponse); + byte[] expectedBytes = Encoding.ASCII.GetBytes(expectedResponse); + + var protocol = new AsciiProtocol(); + + // Act + var bytes = protocol.SerializeReadDeviceIdentification(UNIT_ID, category, ModbusDeviceIdentificationObject.ProductCode); + + // Assert + Assert.IsNotNull(bytes); + CollectionAssert.AreEqual(expectedBytes, bytes.ToArray()); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ShouldThrowOutOfRangeExceptionForCategoryOnSerializeReadDeviceIdentification() + { + // Arrange + var protocol = new AsciiProtocol(); + + // Act + protocol.SerializeReadDeviceIdentification(UNIT_ID, (ModbusDeviceIdentificationCategory)10, ModbusDeviceIdentificationObject.ProductCode); + + // Assert - ArgumentOutOfRangeException + } + + [DataTestMethod] + [DataRow(false)] + [DataRow(true)] + public void ShouldDeserializeReadDeviceIdentification(bool moreAndIndividual) + { + // Arrange + string response = $":{UNIT_ID:X2}2B0E02{(moreAndIndividual ? "82" : "02")}{(moreAndIndividual ? "FF" : "00")}{(moreAndIndividual ? "05" : "00")}010402414D"; + AddTrailer(ref response); + byte[] responseBytes = Encoding.ASCII.GetBytes(response); + + var protocol = new AsciiProtocol(); + + // Act + var result = protocol.DeserializeReadDeviceIdentification(responseBytes); + + // 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 + string response = $":{UNIT_ID:X2}2B0D"; + AddTrailer(ref response); + byte[] responseBytes = Encoding.ASCII.GetBytes(response); + + var protocol = new AsciiProtocol(); + + // Act + protocol.DeserializeReadDeviceIdentification(responseBytes); + } + + [TestMethod] + [ExpectedException(typeof(ModbusException))] + public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForCategory() + { + // Arrange + string response = $":{UNIT_ID:X2}2B0E08"; + AddTrailer(ref response); + byte[] responseBytes = Encoding.ASCII.GetBytes(response); + + var protocol = new AsciiProtocol(); + + // Act + protocol.DeserializeReadDeviceIdentification(responseBytes); + } + + #endregion Read Device Identification + + #region Write Single Coil + + [TestMethod] + public void ShouldSerializeWriteSingleCoil() + { + // Arrange + string expectedResponse = $":{UNIT_ID:X2}05006DFF00"; + AddTrailer(ref expectedResponse); + byte[] expectedBytes = Encoding.ASCII.GetBytes(expectedResponse); + + var coil = new Coil { Address = 109, Value = true }; + var protocol = new AsciiProtocol(); + + // Act + var result = protocol.SerializeWriteSingleCoil(UNIT_ID, coil); + + // Assert + Assert.IsNotNull(result); + CollectionAssert.AreEqual(expectedBytes, result.ToArray()); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void ShouldThrowArgumentNullOnSerializeWriteSingleCoil() + { + // Arrange + var protocol = new AsciiProtocol(); + + // Act + protocol.SerializeWriteSingleCoil(UNIT_ID, null); + + // Assert - ArgumentNullException + } + + [TestMethod] + public void ShouldDeserializeWriteSingleCoil() + { + // Arrange + string response = $":{UNIT_ID:X2}05010AFF00"; + AddTrailer(ref response); + byte[] bytes = Encoding.ASCII.GetBytes(response); + + var protocol = new AsciiProtocol(); + + // 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 + string expectedResponse = $":{UNIT_ID:X2}06006D007B"; + AddTrailer(ref expectedResponse); + byte[] expectedBytes = Encoding.ASCII.GetBytes(expectedResponse); + + var register = new HoldingRegister { Address = 109, Value = 123 }; + var protocol = new AsciiProtocol(); + + // Act + var result = protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, register); + + // Assert + Assert.IsNotNull(result); + CollectionAssert.AreEqual(expectedBytes, result.ToArray()); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void ShouldThrowArgumentNullOnSerializeWriteSingleHoldingRegister() + { + // Arrange + var protocol = new AsciiProtocol(); + + // Act + protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, null); + + // Assert - ArgumentNullException + } + + [TestMethod] + public void ShouldDeserializeWriteSingleHoldingRegister() + { + // Arrange + string response = $":{UNIT_ID:X2}0602020123"; + AddTrailer(ref response); + byte[] bytes = Encoding.ASCII.GetBytes(response); + + var protocol = new AsciiProtocol(); + + // 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 + string expectedResponse = $":{UNIT_ID:X2}0F000A00050115"; + AddTrailer(ref expectedResponse); + byte[] expectedBytes = Encoding.ASCII.GetBytes(expectedResponse); + + 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 AsciiProtocol(); + + // Act + var result = protocol.SerializeWriteMultipleCoils(UNIT_ID, coils); + + // Assert + Assert.IsNotNull(result); + CollectionAssert.AreEqual(expectedBytes, result.ToArray()); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void ShouldThrowArgumentNullOnSerializeWriteMultipleCoils() + { + // Arrange + var protocol = new AsciiProtocol(); + + // 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 AsciiProtocol(); + + // 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 AsciiProtocol(); + + // 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 AsciiProtocol(); + + // Act + protocol.SerializeWriteMultipleCoils(UNIT_ID, coils); + + // Assert - ArgumentException + } + + [TestMethod] + public void ShouldDeserializeWriteMultipleCoils() + { + // Arrange + string response = $":{UNIT_ID:X2}0F010A000B"; + AddTrailer(ref response); + byte[] bytes = Encoding.ASCII.GetBytes(response); + + var protocol = new AsciiProtocol(); + + // 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 + string expectedResponse = $":{UNIT_ID:X2}10000A000204000A000B"; + AddTrailer(ref expectedResponse); + byte[] expectedBytes = Encoding.ASCII.GetBytes(expectedResponse); + + var registers = new HoldingRegister[] + { + new() { Address = 10, Value = 10 }, + new() { Address = 11, Value = 11 } + }; + var protocol = new AsciiProtocol(); + + // Act + var result = protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers); + + // Assert + Assert.IsNotNull(result); + CollectionAssert.AreEqual(expectedBytes, result.ToArray()); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void ShouldThrowArgumentNullOnSerializeWriteMultipleHoldingRegisters() + { + // Arrange + var protocol = new AsciiProtocol(); + + // 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 AsciiProtocol(); + + // 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 AsciiProtocol(); + + // 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 AsciiProtocol(); + + // Act + protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers); + + // Assert - ArgumentException + } + + [TestMethod] + public void ShouldDeserializeWriteMultipleHoldingRegisters() + { + // Arrange + string response = $":{UNIT_ID:X2}10020A000A"; + AddTrailer(ref response); + byte[] bytes = Encoding.ASCII.GetBytes(response); + + var protocol = new AsciiProtocol(); + + // 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 ShouldReturnTrueOnCheckResponseComplete() + { + // Arrange + byte[] bytes = Encoding.ASCII.GetBytes($":{UNIT_ID:X2}0100050002XX\r\n"); + var protocol = new AsciiProtocol(); + + // Act + bool complete = protocol.CheckResponseComplete(bytes); + + // Assert + Assert.IsTrue(complete); + } + + [TestMethod] + public void ShouldReturnFalseForLessBytesOnCheckResponseComplete() + { + // Arrange + byte[] bytes = Encoding.ASCII.GetBytes(":\r"); + var protocol = new AsciiProtocol(); + + // Act + bool complete = protocol.CheckResponseComplete(bytes); + + // Assert + Assert.IsFalse(complete); + } + + [TestMethod] + public void ShouldReturnFalseForMissingCrLfOnCheckResponseComplete() + { + // Arrange + byte[] bytes = Encoding.ASCII.GetBytes($":{UNIT_ID:X2}0100050002XX"); + var protocol = new AsciiProtocol(); + + // Act + bool complete = protocol.CheckResponseComplete(bytes); + + // Assert + Assert.IsFalse(complete); + } + + [DataTestMethod] + [DataRow(0x01)] + [DataRow(0x02)] + [DataRow(0x03)] + [DataRow(0x04)] + public void ShouldValidateReadResponse(int fn) + { + // Arrange + string request = $":{UNIT_ID:X2}{fn:X2}00010001"; + string response = $":{UNIT_ID:X2}{fn:X2}0100"; + AddTrailer(ref response); + var protocol = new AsciiProtocol(); + + // Act + protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)); + } + + [DataTestMethod] + [DataRow(0x05)] + [DataRow(0x06)] + [DataRow(0x0F)] + [DataRow(0x10)] + public void ShouldValidateWriteResponse(int fn) + { + // Arrange + string request = $":{UNIT_ID:X2}{fn:X2}0001FF00"; + string response = $":{UNIT_ID:X2}{fn:X2}0001FF00"; + AddTrailer(ref response); + var protocol = new AsciiProtocol(); + + // Act + protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)); + } + + [TestMethod] + [ExpectedException(typeof(ModbusException))] + public void ShouldThrowForMissingHeaderOnValidateResponse() + { + // Arrange + string request = $":{UNIT_ID:X2}0100010001"; + string response = $"{UNIT_ID:X2}0101009"; + AddTrailer(ref response); + var protocol = new AsciiProtocol(); + + // Act + protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)); + } + + [TestMethod] + [ExpectedException(typeof(ModbusException))] + public void ShouldThrowForMissingTrailerOnValidateResponse() + { + // Arrange + string request = $":{UNIT_ID:X2}0100010001"; + string response = $":{UNIT_ID:X2}010100"; + var protocol = new AsciiProtocol(); + + // Act + protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)); + } + + [TestMethod] + [ExpectedException(typeof(ModbusException))] + public void ShouldThrowForUnitIdOnValidateResponse() + { + // Arrange + string request = $":{UNIT_ID:X2}010001FF00"; + string response = $":{UNIT_ID + 1:X2}010001FF00"; + AddTrailer(ref response); + var protocol = new AsciiProtocol(); + + // Act + protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)); + } + + [TestMethod] + [ExpectedException(typeof(ModbusException))] + public void ShouldThrowForLrcOnValidateResponse() + { + // Arrange + string request = $":{UNIT_ID:X2}010001FF00"; + string response = $":{UNIT_ID:X2}010001FF00XX\r\n"; + var protocol = new AsciiProtocol(); + + // Act + protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)); + } + + [TestMethod] + [ExpectedException(typeof(ModbusException))] + public void ShouldThrowForFunctionCodeOnValidateResponse() + { + // Arrange + string request = $":{UNIT_ID:X2}010001FF00"; + string response = $":{UNIT_ID:X2}020001FF00"; + AddTrailer(ref response); + var protocol = new AsciiProtocol(); + + // Act + protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)); + } + + [TestMethod] + [ExpectedException(typeof(ModbusException))] + public void ShouldThrowForErrorOnValidateResponse() + { + // Arrange + string request = $":{UNIT_ID:X2}010001FF00"; + string response = $":{UNIT_ID:X2}8101"; + AddTrailer(ref response); + var protocol = new AsciiProtocol(); + + // Act + protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)); + } + + [DataTestMethod] + [DataRow(0x01)] + [DataRow(0x02)] + [DataRow(0x03)] + [DataRow(0x04)] + [ExpectedException(typeof(ModbusException))] + public void ShouldThrowForReadLengthOnValidateResponse(int fn) + { + // Arrange + string request = $":{UNIT_ID:X2}{fn:X2}00010002"; + string response = $":{UNIT_ID:X2}{fn:X2}FF0000"; + AddTrailer(ref response); + var protocol = new AsciiProtocol(); + + // Act + protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)); + } + + [DataTestMethod] + [DataRow(0x05)] + [DataRow(0x06)] + [DataRow(0x0F)] + [DataRow(0x10)] + [ExpectedException(typeof(ModbusException))] + public void ShouldThrowForWriteLengthOnValidateResponse(int fn) + { + // Arrange + string request = $":{UNIT_ID:X2}{fn:X2}00010002"; + string response = $":{UNIT_ID:X2}{fn:X2}0013000200"; + AddTrailer(ref response); + var protocol = new AsciiProtocol(); + + // Act + protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)); + } + + [TestMethod] + public void ShouldReturnValidLrc() + { + // Arrange + string msg = "0207"; + + // Act + string lrc = AsciiProtocol.LRC(msg, 0, 4); + + // Assert + Assert.AreEqual("F7", lrc); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + [DataRow("\t")] + [ExpectedException(typeof(ArgumentNullException))] + public void ShouldThrowArgumentNullExceptionForMessageOnLrc(string msg) + { + // Arrange + + // Act + AsciiProtocol.LRC(msg); + + // Assert - ArgumentNullException + } + + [DataTestMethod] + [DataRow(-1)] + [DataRow(4)] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ShouldThrowArgumentOutOfRangeExceptionForStartOnLrc(int start) + { + // Arrange + string msg = "0207"; + + // Act + AsciiProtocol.LRC(msg, start); + + // Assert - ArgumentOutOfRangeException + } + + [DataTestMethod] + [DataRow(0)] + [DataRow(5)] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ShouldThrowArgumentOutOfRangeExceptionForLengthOnLrc(int length) + { + // Arrange + string msg = "0207"; + + // Act + AsciiProtocol.LRC(msg, 0, length); + + // Assert - ArgumentOutOfRangeException + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void ShouldThrowArgumentExceptionForMessageLengthOnLrc() + { + // Arrange + string msg = "0207"; + + // Act + AsciiProtocol.LRC(msg); + + // Assert - ArgumentException + } + + #endregion Validation + + [TestMethod] + public void ShouldNameAscii() + { + // Arrange + var protocol = new AsciiProtocol(); + + // Act + string result = protocol.Name; + + // Assert + Assert.AreEqual("ASCII", result); + } + + private static void AddTrailer(ref string str) + { + string lrc = AsciiProtocol.LRC(str); + str += lrc; + str += "\r\n"; + } + } +} diff --git a/AMWD.Protocols.Modbus.Tests/Common/Protocols/RtuProtocolTest.cs b/AMWD.Protocols.Modbus.Tests/Common/Protocols/RtuProtocolTest.cs index f9f06e7..aae7ded 100644 --- a/AMWD.Protocols.Modbus.Tests/Common/Protocols/RtuProtocolTest.cs +++ b/AMWD.Protocols.Modbus.Tests/Common/Protocols/RtuProtocolTest.cs @@ -40,19 +40,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols // 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)] @@ -152,19 +139,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols // 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)] @@ -264,19 +238,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols // 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)] @@ -371,19 +332,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols // 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)] @@ -484,19 +432,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols // 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() @@ -592,19 +527,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols // 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() @@ -669,19 +591,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols // 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() @@ -759,19 +668,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols // 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() @@ -903,19 +799,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols // 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() @@ -1187,26 +1070,34 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols Assert.IsTrue(complete); } - [TestMethod] - public void ShouldValidateReadResponse() + [DataTestMethod] + [DataRow(0x01)] + [DataRow(0x02)] + [DataRow(0x03)] + [DataRow(0x04)] + public void ShouldValidateReadResponse(int fn) { // Arrange - byte[] request = [UNIT_ID, 0x01, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK - byte[] response = [UNIT_ID, 0x01, 0x01, 0x00, 0x00, 0x00]; - AddCrc(response); + byte[] request = [UNIT_ID, (byte)fn, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK + byte[] response = [UNIT_ID, (byte)fn, 0x01, 0x00, 0x00, 0x00]; + SetCrc(response); var protocol = new RtuProtocol(); // Act protocol.ValidateResponse(request, response); } - [TestMethod] - public void ShouldValidateWriteResponse() + [DataTestMethod] + [DataRow(0x05)] + [DataRow(0x06)] + [DataRow(0x0F)] + [DataRow(0x10)] + public void ShouldValidateWriteResponse(int fn) { // 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); + byte[] request = [UNIT_ID, (byte)fn, 0x00, 0x01, 0xFF, 0x00]; // CRC missing, OK + byte[] response = [UNIT_ID, (byte)fn, 0x00, 0x01, 0xFF, 0x00, 0x00, 0x00]; + SetCrc(response); var protocol = new RtuProtocol(); // Act @@ -1220,7 +1111,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols // 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); + SetCrc(response); var protocol = new RtuProtocol(); // Act @@ -1249,7 +1140,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols // Arrange byte[] request = [UNIT_ID, 0x01, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK byte[] response = [UNIT_ID, 0x02, 0x01, 0x00, 0x00, 0x00]; - AddCrc(response); + SetCrc(response); var protocol = new RtuProtocol(); // Act @@ -1263,7 +1154,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols // Arrange byte[] request = [UNIT_ID, 0x01, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK byte[] response = [UNIT_ID, 0x81, 0x01, 0x00, 0x00]; - AddCrc(response); + SetCrc(response); var protocol = new RtuProtocol(); // Act @@ -1281,7 +1172,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols // 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); + SetCrc(response); var protocol = new RtuProtocol(); // Act @@ -1299,7 +1190,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols // 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); + SetCrc(response); var protocol = new RtuProtocol(); // Act @@ -1380,7 +1271,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols Assert.AreEqual("RTU", result); } - private static void AddCrc(byte[] bytes) + private static void SetCrc(byte[] bytes) { byte[] crc = RtuProtocol.CRC16(bytes, 0, bytes.Length - 2); bytes[^2] = crc[0]; diff --git a/README.md b/README.md index 1319739..62096cc 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ It uses a specific TCP connection implementation and plugs all things from the C --- -Published under [MIT License] (see [**tl;dr**Legal]) +Published under [MIT License] (see [**tl;dr**Legal]) +[![built with Codeium](https://codeium.com/badges/main)](https://codeium.com)