Implementing ASCII protocol; removing min. unit ID for serial line - see note on README
This commit is contained in:
733
AMWD.Protocols.Modbus.Common/Protocols/AsciiProtocol.cs
Normal file
733
AMWD.Protocols.Modbus.Common/Protocols/AsciiProtocol.cs
Normal 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 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
1112
AMWD.Protocols.Modbus.Tests/Common/Protocols/AsciiProtocolTest.cs
Normal file
1112
AMWD.Protocols.Modbus.Tests/Common/Protocols/AsciiProtocolTest.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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];
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user