Implementing ASCII protocol; removing min. unit ID for serial line - see note on README

This commit is contained in:
2024-03-28 18:08:44 +01:00
parent dee0d67453
commit 1bcac96d52
7 changed files with 1949 additions and 192 deletions

View File

@@ -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
{
/// <summary>
/// Default implementation of the Modbus ASCII protocol.
/// </summary>
public class AsciiProtocol : IModbusProtocol
{
#region Constants
/// <summary>
/// The minimum allowed unit id specified by the Modbus SerialLine protocol.
/// </summary>
/// <remarks>
/// <strong>INFORMATION:</strong>
/// <br/>
/// Reading the specification, the minimum allowed unit ID would be <strong>1</strong>.
/// <br/>
/// As of other implementations seen, this limit is <em>not</em> enforced!
/// </remarks>
public const byte MIN_UNIT_ID = 0x01;
/// <summary>
/// The maximum allowed unit id specified by the Modbus SerialLine protocol.
/// </summary>
/// <remarks>
/// Reading the specification, the max allowed unit id would be <strong>247</strong>!
/// </remarks>
public const byte MAX_UNIT_ID = 0xFF;
/// <summary>
/// The minimum allowed read count specified by the Modbus SerialLine protocol.
/// </summary>
public const ushort MIN_READ_COUNT = 0x01;
/// <summary>
/// The minimum allowed write count specified by the Modbus SerialLine protocol.
/// </summary>
public const ushort MIN_WRITE_COUNT = 0x01;
/// <summary>
/// The maximum allowed read count for discrete values specified by the Modbus SerialLine protocol.
/// </summary>
public const ushort MAX_DISCRETE_READ_COUNT = 0x07D0; // 2000
/// <summary>
/// The maximum allowed write count for discrete values specified by the Modbus SerialLine protocol.
/// </summary>
public const ushort MAX_DISCRETE_WRITE_COUNT = 0x07B0; // 1968
/// <summary>
/// The maximum allowed read count for registers specified by the Modbus SerialLine protocol.
/// </summary>
public const ushort MAX_REGISTER_READ_COUNT = 0x007D; // 125
/// <summary>
/// The maximum allowed write count for registers specified by the Modbus SerialLine protocol.
/// </summary>
public const ushort MAX_REGISTER_WRITE_COUNT = 0x007B; // 123
/// <summary>
/// The maximum allowed ADU length in chars.
/// </summary>
/// <remarks>
/// 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).
/// </remarks>
public const int MAX_ADU_LENGTH = 513; // chars in ASCII (so bytes in the end)
#endregion Constants
/// <inheritdoc/>
public string Name => "ASCII";
#region Read
/// <inheritdoc/>
public IReadOnlyList<byte> 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);
}
/// <inheritdoc/>
public IReadOnlyList<Coil> DeserializeReadCoils(IReadOnlyList<byte> 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<Coil>();
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;
}
/// <inheritdoc/>
public IReadOnlyList<byte> 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);
}
/// <inheritdoc/>
public IReadOnlyList<DiscreteInput> DeserializeReadDiscreteInputs(IReadOnlyList<byte> 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<DiscreteInput>();
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;
}
/// <inheritdoc/>
public IReadOnlyList<byte> 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);
}
/// <inheritdoc/>
public IReadOnlyList<HoldingRegister> DeserializeReadHoldingRegisters(IReadOnlyList<byte> 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<HoldingRegister>();
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;
}
/// <inheritdoc/>
public IReadOnlyList<byte> 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);
}
/// <inheritdoc/>
public IReadOnlyList<InputRegister> DeserializeReadInputRegisters(IReadOnlyList<byte> 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<InputRegister>();
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;
}
/// <inheritdoc/>
public IReadOnlyList<byte> 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);
}
/// <inheritdoc/>
public DeviceIdentificationRaw DeserializeReadDeviceIdentification(IReadOnlyList<byte> 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
/// <inheritdoc/>
public IReadOnlyList<byte> 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);
}
/// <inheritdoc/>
public Coil DeserializeWriteSingleCoil(IReadOnlyList<byte> 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))
};
}
/// <inheritdoc/>
public IReadOnlyList<byte> 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);
}
/// <inheritdoc/>
public HoldingRegister DeserializeWriteSingleHoldingRegister(IReadOnlyList<byte> 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))
};
}
/// <inheritdoc/>
public IReadOnlyList<byte> SerializeWriteMultipleCoils(byte unitId, IReadOnlyList<Coil> 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);
}
/// <inheritdoc/>
public (ushort FirstAddress, ushort NumberOfCoils) DeserializeWriteMultipleCoils(IReadOnlyList<byte> 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);
}
/// <inheritdoc/>
public IReadOnlyList<byte> SerializeWriteMultipleHoldingRegisters(byte unitId, IReadOnlyList<HoldingRegister> 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);
}
/// <inheritdoc/>
public (ushort FirstAddress, ushort NumberOfRegisters) DeserializeWriteMultipleHoldingRegisters(IReadOnlyList<byte> 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
/// <inheritdoc/>
public bool CheckResponseComplete(IReadOnlyList<byte> 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;
}
/// <inheritdoc/>
public void ValidateResponse(IReadOnlyList<byte> request, IReadOnlyList<byte> 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?
}
/// <summary>
/// Calculate LRC for Modbus ASCII.
/// </summary>
/// <param name="message">The message chars.</param>
/// <param name="start">The start index.</param>
/// <param name="length">The number of bytes to calculate.</param>
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 8bit 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
}
}

View File

@@ -13,12 +13,19 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
#region Constants #region Constants
/// <summary> /// <summary>
/// The minimum allowed unit id specified by the Modbus TCP protocol. /// The minimum allowed unit id specified by the Modbus SerialLine protocol.
/// </summary> /// </summary>
/// <remarks>
/// <strong>INFORMATION:</strong>
/// <br/>
/// Reading the specification, the minimum allowed unit ID would be <strong>1</strong>.
/// <br/>
/// As of other implementations seen, this limit is <em>not</em> enforced!
/// </remarks>
public const byte MIN_UNIT_ID = 0x01; public const byte MIN_UNIT_ID = 0x01;
/// <summary> /// <summary>
/// The maximum allowed unit id specified by the Modbus TCP protocol. /// The maximum allowed unit id specified by the Modbus SerialLine protocol.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Reading the specification, the max allowed unit id would be <strong>247</strong>! /// Reading the specification, the max allowed unit id would be <strong>247</strong>!
@@ -26,32 +33,32 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
public const byte MAX_UNIT_ID = 0xFF; public const byte MAX_UNIT_ID = 0xFF;
/// <summary> /// <summary>
/// The minimum allowed read count specified by the Modbus TCP protocol. /// The minimum allowed read count specified by the Modbus SerialLine protocol.
/// </summary> /// </summary>
public const ushort MIN_READ_COUNT = 0x01; public const ushort MIN_READ_COUNT = 0x01;
/// <summary> /// <summary>
/// The minimum allowed write count specified by the Modbus TCP protocol. /// The minimum allowed write count specified by the Modbus SerialLine protocol.
/// </summary> /// </summary>
public const ushort MIN_WRITE_COUNT = 0x01; public const ushort MIN_WRITE_COUNT = 0x01;
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public const ushort MAX_DISCRETE_READ_COUNT = 0x07D0; // 2000 public const ushort MAX_DISCRETE_READ_COUNT = 0x07D0; // 2000
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public const ushort MAX_DISCRETE_WRITE_COUNT = 0x07B0; // 1968 public const ushort MAX_DISCRETE_WRITE_COUNT = 0x07B0; // 1968
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public const ushort MAX_REGISTER_READ_COUNT = 0x007D; // 125 public const ushort MAX_REGISTER_READ_COUNT = 0x007D; // 125
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public const ushort MAX_REGISTER_WRITE_COUNT = 0x007B; // 123 public const ushort MAX_REGISTER_WRITE_COUNT = 0x007B; // 123
@@ -74,9 +81,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
/// <inheritdoc/> /// <inheritdoc/>
public IReadOnlyList<byte> SerializeReadCoils(byte unitId, ushort startAddress, ushort count) public IReadOnlyList<byte> 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) if (count < MIN_READ_COUNT || MAX_DISCRETE_READ_COUNT < count)
throw new ArgumentOutOfRangeException(nameof(count)); throw new ArgumentOutOfRangeException(nameof(count));
@@ -137,9 +141,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
/// <inheritdoc/> /// <inheritdoc/>
public IReadOnlyList<byte> SerializeReadDiscreteInputs(byte unitId, ushort startAddress, ushort count) public IReadOnlyList<byte> 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) if (count < MIN_READ_COUNT || MAX_DISCRETE_READ_COUNT < count)
throw new ArgumentOutOfRangeException(nameof(count)); throw new ArgumentOutOfRangeException(nameof(count));
@@ -200,9 +201,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
/// <inheritdoc/> /// <inheritdoc/>
public IReadOnlyList<byte> SerializeReadHoldingRegisters(byte unitId, ushort startAddress, ushort count) public IReadOnlyList<byte> 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) if (count < MIN_READ_COUNT || MAX_REGISTER_READ_COUNT < count)
throw new ArgumentOutOfRangeException(nameof(count)); throw new ArgumentOutOfRangeException(nameof(count));
@@ -260,9 +258,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
/// <inheritdoc/> /// <inheritdoc/>
public IReadOnlyList<byte> SerializeReadInputRegisters(byte unitId, ushort startAddress, ushort count) public IReadOnlyList<byte> 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) if (count < MIN_READ_COUNT || MAX_REGISTER_READ_COUNT < count)
throw new ArgumentOutOfRangeException(nameof(count)); throw new ArgumentOutOfRangeException(nameof(count));
@@ -320,9 +315,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
/// <inheritdoc/> /// <inheritdoc/>
public IReadOnlyList<byte> SerializeReadDeviceIdentification(byte unitId, ModbusDeviceIdentificationCategory category, ModbusDeviceIdentificationObject objectId) public IReadOnlyList<byte> SerializeReadDeviceIdentification(byte unitId, ModbusDeviceIdentificationCategory category, ModbusDeviceIdentificationObject objectId)
{ {
if (unitId < MIN_UNIT_ID)
throw new ArgumentOutOfRangeException(nameof(unitId));
if (!Enum.IsDefined(typeof(ModbusDeviceIdentificationCategory), category)) if (!Enum.IsDefined(typeof(ModbusDeviceIdentificationCategory), category))
throw new ArgumentOutOfRangeException(nameof(category)); throw new ArgumentOutOfRangeException(nameof(category));
@@ -387,9 +379,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
/// <inheritdoc/> /// <inheritdoc/>
public IReadOnlyList<byte> SerializeWriteSingleCoil(byte unitId, Coil coil) public IReadOnlyList<byte> SerializeWriteSingleCoil(byte unitId, Coil coil)
{ {
if (unitId < MIN_UNIT_ID)
throw new ArgumentOutOfRangeException(nameof(unitId));
#if NET8_0_OR_GREATER #if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(coil); ArgumentNullException.ThrowIfNull(coil);
#else #else
@@ -434,9 +423,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
/// <inheritdoc/> /// <inheritdoc/>
public IReadOnlyList<byte> SerializeWriteSingleHoldingRegister(byte unitId, HoldingRegister register) public IReadOnlyList<byte> SerializeWriteSingleHoldingRegister(byte unitId, HoldingRegister register)
{ {
if (unitId < MIN_UNIT_ID)
throw new ArgumentOutOfRangeException(nameof(unitId));
#if NET8_0_OR_GREATER #if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(register); ArgumentNullException.ThrowIfNull(register);
#else #else
@@ -481,9 +467,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
/// <inheritdoc/> /// <inheritdoc/>
public IReadOnlyList<byte> SerializeWriteMultipleCoils(byte unitId, IReadOnlyList<Coil> coils) public IReadOnlyList<byte> SerializeWriteMultipleCoils(byte unitId, IReadOnlyList<Coil> coils)
{ {
if (unitId < MIN_UNIT_ID)
throw new ArgumentOutOfRangeException(nameof(unitId));
#if NET8_0_OR_GREATER #if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(coils); ArgumentNullException.ThrowIfNull(coils);
#else #else
@@ -555,9 +538,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
/// <inheritdoc/> /// <inheritdoc/>
public IReadOnlyList<byte> SerializeWriteMultipleHoldingRegisters(byte unitId, IReadOnlyList<HoldingRegister> registers) public IReadOnlyList<byte> SerializeWriteMultipleHoldingRegisters(byte unitId, IReadOnlyList<HoldingRegister> registers)
{ {
if (unitId < MIN_UNIT_ID)
throw new ArgumentOutOfRangeException(nameof(unitId));
#if NET8_0_OR_GREATER #if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(registers); ArgumentNullException.ThrowIfNull(registers);
#else #else

View File

@@ -91,9 +91,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
byte[] request = new byte[12]; byte[] request = new byte[12];
byte[] header = GetHeader(unitId, 6); byte[] header = GetHeader(6);
Array.Copy(header, 0, request, 0, header.Length); Array.Copy(header, 0, request, 0, header.Length);
// Unit id
request[6] = unitId;
// Function code // Function code
request[7] = (byte)ModbusFunctionCode.ReadCoils; request[7] = (byte)ModbusFunctionCode.ReadCoils;
@@ -146,9 +149,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
byte[] request = new byte[12]; byte[] request = new byte[12];
byte[] header = GetHeader(unitId, 6); byte[] header = GetHeader(6);
Array.Copy(header, 0, request, 0, header.Length); Array.Copy(header, 0, request, 0, header.Length);
// Unit id
request[6] = unitId;
// Function code // Function code
request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs; request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
@@ -201,9 +207,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
byte[] request = new byte[12]; byte[] request = new byte[12];
byte[] header = GetHeader(unitId, 6); byte[] header = GetHeader(6);
Array.Copy(header, 0, request, 0, header.Length); Array.Copy(header, 0, request, 0, header.Length);
// Unit id
request[6] = unitId;
// Function code // Function code
request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters; request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
@@ -253,9 +262,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
byte[] request = new byte[12]; byte[] request = new byte[12];
byte[] header = GetHeader(unitId, 6); byte[] header = GetHeader(6);
Array.Copy(header, 0, request, 0, header.Length); Array.Copy(header, 0, request, 0, header.Length);
// Unit id
request[6] = unitId;
// Function code // Function code
request[7] = (byte)ModbusFunctionCode.ReadInputRegisters; request[7] = (byte)ModbusFunctionCode.ReadInputRegisters;
@@ -302,9 +314,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
byte[] request = new byte[11]; byte[] request = new byte[11];
byte[] header = GetHeader(unitId, 5); byte[] header = GetHeader(5);
Array.Copy(header, 0, request, 0, header.Length); Array.Copy(header, 0, request, 0, header.Length);
// Unit id
request[6] = unitId;
// Function code // Function code
request[7] = (byte)ModbusFunctionCode.EncapsulatedInterface; request[7] = (byte)ModbusFunctionCode.EncapsulatedInterface;
@@ -365,9 +380,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
byte[] request = new byte[12]; byte[] request = new byte[12];
byte[] header = GetHeader(unitId, 6); byte[] header = GetHeader(6);
Array.Copy(header, 0, request, 0, header.Length); Array.Copy(header, 0, request, 0, header.Length);
// Unit id
request[6] = unitId;
// Function code // Function code
request[7] = (byte)ModbusFunctionCode.WriteSingleCoil; request[7] = (byte)ModbusFunctionCode.WriteSingleCoil;
@@ -404,9 +422,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
byte[] request = new byte[12]; byte[] request = new byte[12];
byte[] header = GetHeader(unitId, 6); byte[] header = GetHeader(6);
Array.Copy(header, 0, request, 0, header.Length); Array.Copy(header, 0, request, 0, header.Length);
// Unit id
request[6] = unitId;
// Function code // Function code
request[7] = (byte)ModbusFunctionCode.WriteSingleRegister; 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 byteCount = (byte)Math.Ceiling(orderedList.Count / 8.0);
byte[] request = new byte[13 + byteCount]; 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); Array.Copy(header, 0, request, 0, header.Length);
// Unit id
request[6] = unitId;
// Function code
request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils; request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils;
// Starting address
byte[] addrBytes = firstAddress.ToBigEndianBytes(); byte[] addrBytes = firstAddress.ToBigEndianBytes();
request[8] = addrBytes[0]; request[8] = addrBytes[0];
request[9] = addrBytes[1]; request[9] = addrBytes[1];
// Quantity
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
request[10] = countBytes[0]; request[10] = countBytes[0];
request[11] = countBytes[1]; request[11] = countBytes[1];
// Byte count
request[12] = byteCount; request[12] = byteCount;
// Coils
int baseOffset = 13; int baseOffset = 13;
for (int i = 0; i < orderedList.Count; i++) 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 byteCount = (byte)(orderedList.Count * 2);
byte[] request = new byte[13 + byteCount]; 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); Array.Copy(header, 0, request, 0, header.Length);
// Unit id
request[6] = unitId;
// Function code
request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters; request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
// Starting address
byte[] addrBytes = firstAddress.ToBigEndianBytes(); byte[] addrBytes = firstAddress.ToBigEndianBytes();
request[8] = addrBytes[0]; request[8] = addrBytes[0];
request[9] = addrBytes[1]; request[9] = addrBytes[1];
// Quantity
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
request[10] = countBytes[0]; request[10] = countBytes[0];
request[11] = countBytes[1]; request[11] = countBytes[1];
// Byte count
request[12] = byteCount; request[12] = byteCount;
// Registers
int baseOffset = 13; int baseOffset = 13;
for (int i = 0; i < orderedList.Count; i++) for (int i = 0; i < orderedList.Count; i++)
{ {
@@ -633,15 +670,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
/// <summary> /// <summary>
/// Generates the header for a Modbus request. /// Generates the header for a Modbus request.
/// </summary> /// </summary>
/// <param name="unitId">The unit identifier.</param>
/// <param name="followingBytes">The number of following bytes.</param> /// <param name="followingBytes">The number of following bytes.</param>
/// <returns>The header ready to copy to the request bytes.</returns> /// <returns>The header ready to copy to the request bytes (6 bytes).</returns>
/// <remarks> private byte[] GetHeader(int followingBytes)
/// <strong>ATTENTION:</strong> Do not forget the <paramref name="unitId"/>. It is placed after the count information.
/// </remarks>
private byte[] GetHeader(byte unitId, int followingBytes)
{ {
byte[] header = new byte[7]; byte[] header = new byte[6];
// Transaction id // Transaction id
ushort txId = GetNextTransacitonId(); ushort txId = GetNextTransacitonId();
@@ -658,9 +691,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
header[4] = countBytes[0]; header[4] = countBytes[0];
header[5] = countBytes[1]; header[5] = countBytes[1];
// Unit identifier
header[6] = unitId;
return header; return header;
} }

View File

@@ -13,13 +13,19 @@ If you want to speak a custom type of protocol with the clients, you can impleme
**ModbusBaseClient** **ModbusBaseClient**
This abstract base client contains all the basic methods and handlings required to communicate via Modbus Protocol. 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 ### Enums
Here you have all typed enumerables defined by the Modbus Protocol. 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 ### Extensions
@@ -41,7 +47,7 @@ The different types handled by the Modbus Protocol.
- Input Register - Input Register
In addition, you'll find the `DeviceIdentification` there. 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. 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. Here you have the specific default implementations for the Modbus Protocol.
- ASCII _(in progress)_ - ASCII
- RTU _(in progress)_ - RTU
- RTU over TCP _(in progress)_
- TCP - 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.
--- ---

File diff suppressed because it is too large Load Diff

View File

@@ -40,19 +40,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
// CRC check will be ignored // 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] [DataTestMethod]
[DataRow(0)] [DataRow(0)]
[DataRow(2001)] [DataRow(2001)]
@@ -152,19 +139,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
// CRC check will be ignored // 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] [DataTestMethod]
[DataRow(0)] [DataRow(0)]
[DataRow(2001)] [DataRow(2001)]
@@ -264,19 +238,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
// CRC check will be ignored // 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] [DataTestMethod]
[DataRow(0)] [DataRow(0)]
[DataRow(126)] [DataRow(126)]
@@ -371,19 +332,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
// CRC check will be ignored // 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] [DataTestMethod]
[DataRow(0)] [DataRow(0)]
[DataRow(126)] [DataRow(126)]
@@ -484,19 +432,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
// CRC check will be ignored // 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] [TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))] [ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ShouldThrowOutOfRangeExceptionForCategoryOnSerializeReadDeviceIdentification() public void ShouldThrowOutOfRangeExceptionForCategoryOnSerializeReadDeviceIdentification()
@@ -592,19 +527,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
// CRC check will be ignored // 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] [TestMethod]
[ExpectedException(typeof(ArgumentNullException))] [ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowArgumentNullOnSerializeWriteSingleCoil() public void ShouldThrowArgumentNullOnSerializeWriteSingleCoil()
@@ -669,19 +591,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
// CRC check will be ignored // 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] [TestMethod]
[ExpectedException(typeof(ArgumentNullException))] [ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowArgumentNullOnSerializeWriteSingleHoldingRegister() public void ShouldThrowArgumentNullOnSerializeWriteSingleHoldingRegister()
@@ -759,19 +668,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
// CRC check will be ignored // CRC check will be ignored
} }
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ShouldThrowArgumentOutOfRangeForUnitIdOnSerializeWriteMultipleCoils()
{
// Arrange
var protocol = new RtuProtocol();
// Act
protocol.SerializeWriteMultipleCoils(0x00, new List<Coil>());
// Assert - ArgumentOutOfRangeException
}
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentNullException))] [ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowArgumentNullOnSerializeWriteMultipleCoils() public void ShouldThrowArgumentNullOnSerializeWriteMultipleCoils()
@@ -903,19 +799,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
// CRC check will be ignored // CRC check will be ignored
} }
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ShouldThrowArgumentOutOfRangeForUnitIdOnSerializeWriteMultipleHoldingRegisters()
{
// Arrange
var protocol = new RtuProtocol();
// Act
protocol.SerializeWriteMultipleHoldingRegisters(0x00, new List<HoldingRegister>());
// Assert - ArgumentOutOfRangeException
}
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentNullException))] [ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowArgumentNullOnSerializeWriteMultipleHoldingRegisters() public void ShouldThrowArgumentNullOnSerializeWriteMultipleHoldingRegisters()
@@ -1187,26 +1070,34 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
Assert.IsTrue(complete); Assert.IsTrue(complete);
} }
[TestMethod] [DataTestMethod]
public void ShouldValidateReadResponse() [DataRow(0x01)]
[DataRow(0x02)]
[DataRow(0x03)]
[DataRow(0x04)]
public void ShouldValidateReadResponse(int fn)
{ {
// Arrange // Arrange
byte[] request = [UNIT_ID, 0x01, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK byte[] request = [UNIT_ID, (byte)fn, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK
byte[] response = [UNIT_ID, 0x01, 0x01, 0x00, 0x00, 0x00]; byte[] response = [UNIT_ID, (byte)fn, 0x01, 0x00, 0x00, 0x00];
AddCrc(response); SetCrc(response);
var protocol = new RtuProtocol(); var protocol = new RtuProtocol();
// Act // Act
protocol.ValidateResponse(request, response); protocol.ValidateResponse(request, response);
} }
[TestMethod] [DataTestMethod]
public void ShouldValidateWriteResponse() [DataRow(0x05)]
[DataRow(0x06)]
[DataRow(0x0F)]
[DataRow(0x10)]
public void ShouldValidateWriteResponse(int fn)
{ {
// Arrange // Arrange
byte[] request = [UNIT_ID, 0x05, 0x00, 0x01, 0xFF, 0x00]; // CRC missing, OK byte[] request = [UNIT_ID, (byte)fn, 0x00, 0x01, 0xFF, 0x00]; // CRC missing, OK
byte[] response = [UNIT_ID, 0x05, 0x00, 0x01, 0xFF, 0x00, 0x00, 0x00]; byte[] response = [UNIT_ID, (byte)fn, 0x00, 0x01, 0xFF, 0x00, 0x00, 0x00];
AddCrc(response); SetCrc(response);
var protocol = new RtuProtocol(); var protocol = new RtuProtocol();
// Act // Act
@@ -1220,7 +1111,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
// Arrange // Arrange
byte[] request = [UNIT_ID, 0x01, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK byte[] request = [UNIT_ID, 0x01, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK
byte[] response = [UNIT_ID + 1, 0x01, 0x01, 0x00, 0x00, 0x00]; byte[] response = [UNIT_ID + 1, 0x01, 0x01, 0x00, 0x00, 0x00];
AddCrc(response); SetCrc(response);
var protocol = new RtuProtocol(); var protocol = new RtuProtocol();
// Act // Act
@@ -1249,7 +1140,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
// Arrange // Arrange
byte[] request = [UNIT_ID, 0x01, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK byte[] request = [UNIT_ID, 0x01, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK
byte[] response = [UNIT_ID, 0x02, 0x01, 0x00, 0x00, 0x00]; byte[] response = [UNIT_ID, 0x02, 0x01, 0x00, 0x00, 0x00];
AddCrc(response); SetCrc(response);
var protocol = new RtuProtocol(); var protocol = new RtuProtocol();
// Act // Act
@@ -1263,7 +1154,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
// Arrange // Arrange
byte[] request = [UNIT_ID, 0x01, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK byte[] request = [UNIT_ID, 0x01, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK
byte[] response = [UNIT_ID, 0x81, 0x01, 0x00, 0x00]; byte[] response = [UNIT_ID, 0x81, 0x01, 0x00, 0x00];
AddCrc(response); SetCrc(response);
var protocol = new RtuProtocol(); var protocol = new RtuProtocol();
// Act // Act
@@ -1281,7 +1172,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
// Arrange // Arrange
byte[] request = [UNIT_ID, (byte)fn, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK byte[] request = [UNIT_ID, (byte)fn, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK
byte[] response = [UNIT_ID, (byte)fn, 0xFF, 0x00, 0x00, 0x00, 0x00]; byte[] response = [UNIT_ID, (byte)fn, 0xFF, 0x00, 0x00, 0x00, 0x00];
AddCrc(response); SetCrc(response);
var protocol = new RtuProtocol(); var protocol = new RtuProtocol();
// Act // Act
@@ -1299,7 +1190,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
// Arrange // Arrange
byte[] request = [UNIT_ID, (byte)fn, 0x00, 0x01, 0x00, 0x02]; // CRC missing, OK 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]; byte[] response = [UNIT_ID, (byte)fn, 0x00, 0x13, 0x00, 0x02, 0x00, 0x00, 0x00];
AddCrc(response); SetCrc(response);
var protocol = new RtuProtocol(); var protocol = new RtuProtocol();
// Act // Act
@@ -1380,7 +1271,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
Assert.AreEqual("RTU", result); 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); byte[] crc = RtuProtocol.CRC16(bytes, 0, bytes.Length - 2);
bytes[^2] = crc[0]; bytes[^2] = crc[0];

View File

@@ -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)