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])
+[](https://codeium.com)