Implemented Device Identification (0x2B / 43) for TCP

This commit is contained in:
2024-03-09 21:31:33 +01:00
parent 7a3c7625a7
commit 24f7cc74a7
9 changed files with 596 additions and 37 deletions

View File

@@ -78,6 +78,22 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
/// <returns>A list of <see cref="InputRegister"/>s.</returns> /// <returns>A list of <see cref="InputRegister"/>s.</returns>
IReadOnlyList<InputRegister> DeserializeReadInputRegisters(IReadOnlyList<byte> response); IReadOnlyList<InputRegister> DeserializeReadInputRegisters(IReadOnlyList<byte> response);
/// <summary>
/// Serializes a read request for device identification.
/// </summary>
/// <param name="unitId">The unit id.</param>
/// <param name="category">The identification category to read.</param>
/// <param name="objectId">The first object id to read.</param>
/// <returns>The <see langword="byte"/>s to send.</returns>
IReadOnlyList<byte> SerializeReadDeviceIdentification(byte unitId, ModbusDeviceIdentificationCategory category, ModbusDeviceIdentificationObject objectId);
/// <summary>
/// Deserializes a read response for device identification.
/// </summary>
/// <param name="response">The <see langword="byte"/>s received.</param>
/// <returns>The raw device identification information data.</returns>
DeviceIdentificationRaw DeserializeReadDeviceIdentification(IReadOnlyList<byte> response);
#endregion Read #endregion Read
#region Write #region Write

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Threading; using System.Threading;
using System.Linq; using System.Linq;
using System.Text;
namespace AMWD.Protocols.Modbus.Common.Contracts namespace AMWD.Protocols.Modbus.Common.Contracts
{ {
@@ -176,6 +177,91 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
return inputRegisters; return inputRegisters;
} }
/// <summary>
/// Read the device identification.
/// </summary>
/// <returns></returns>
/// <remarks>
/// The interface consists of three (3) categories of objects:
/// <list type="bullet">
/// <item>
/// <em>Basic Device Identification</em><br/>
/// All objects of this category are mandatory: VendorName, ProductCode and RevisionNumber.
/// </item>
/// <item>
/// <em>Regular Device Identification</em><br/>
/// In addition to basic data objects, the device provides additional and optional identification and description data objects.
/// All of the objects of this category are defined in the standard but their implementation is optional.
/// </item>
/// <item>
/// <em>Extended Device Identification</em><br/>
/// In addition to regular data objects, the device provides additional and optional identification and description private data about the physical device itself.
/// All of these data are device dependent.
/// </item>
/// </list>
/// </remarks>
public virtual async Task<DeviceIdentification> ReadDeviceIdentificationAsync(byte unitId, ModbusDeviceIdentificationCategory category, ModbusDeviceIdentificationObject objectId = 0x00, CancellationToken cancellationToken = default)
{
Assertions();
ModbusDeviceIdentificationObject requestObjectId = objectId;
var devIdent = new DeviceIdentification();
DeviceIdentificationRaw result;
do
{
var request = Protocol.SerializeReadDeviceIdentification(unitId, category, requestObjectId);
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
Protocol.ValidateResponse(request, response);
result = Protocol.DeserializeReadDeviceIdentification(response);
devIdent.IsIndividualAccessAllowed = result.AllowsIndividualAccess;
foreach (var item in result.Objects)
{
switch ((ModbusDeviceIdentificationObject)item.Key)
{
case ModbusDeviceIdentificationObject.VendorName:
devIdent.VendorName = Encoding.ASCII.GetString(item.Value);
break;
case ModbusDeviceIdentificationObject.ProductCode:
devIdent.ProductCode = Encoding.ASCII.GetString(item.Value);
break;
case ModbusDeviceIdentificationObject.MajorMinorRevision:
devIdent.MajorMinorRevision = Encoding.ASCII.GetString(item.Value);
break;
case ModbusDeviceIdentificationObject.VendorUrl:
devIdent.VendorUrl = Encoding.ASCII.GetString(item.Value);
break;
case ModbusDeviceIdentificationObject.ProductName:
devIdent.ProductName = Encoding.ASCII.GetString(item.Value);
break;
case ModbusDeviceIdentificationObject.ModelName:
devIdent.ModelName = Encoding.ASCII.GetString(item.Value);
break;
case ModbusDeviceIdentificationObject.UserApplicationName:
devIdent.UserApplicationName = Encoding.ASCII.GetString(item.Value);
break;
default:
devIdent.ExtendedObjects.Add(item.Key, item.Value);
break;
}
}
requestObjectId = (ModbusDeviceIdentificationObject)result.NextObjectIdToRequest;
}
while (result.MoreRequestsNeeded);
return devIdent;
}
/// <summary> /// <summary>
/// Writes a single <see cref="Coil"/>. /// Writes a single <see cref="Coil"/>.
/// </summary> /// </summary>

View File

@@ -0,0 +1,34 @@
using System.ComponentModel;
namespace AMWD.Protocols.Modbus.Common
{
/// <summary>
/// List of known categories for Modbus device identification.
/// </summary>
public enum ModbusDeviceIdentificationCategory : byte
{
/// <summary>
/// The basic information. These are mandatory.
/// </summary>
[Description("Basic Device Identification")]
Basic = 0x01,
/// <summary>
/// The regular information. These are optional.
/// </summary>
[Description("Regular Device Identification")]
Regular = 0x02,
/// <summary>
/// The extended information. These are optional too.
/// </summary>
[Description("Extended Device Identification")]
Extended = 0x03,
/// <summary>
/// Request to a specific identification object.
/// </summary>
[Description("Request to a specific identification object")]
Individual = 0x04
}
}

View File

@@ -0,0 +1,43 @@
namespace AMWD.Protocols.Modbus.Common
{
/// <summary>
/// List of known object ids for Modbus device identification.
/// </summary>
public enum ModbusDeviceIdentificationObject : byte
{
/// <summary>
/// The vendor name (mandatory).
/// </summary>
VendorName = 0x00,
/// <summary>
/// The product code (mandatory).
/// </summary>
ProductCode = 0x01,
/// <summary>
/// The version in major, minor and revision number (mandatory).
/// </summary>
MajorMinorRevision = 0x02,
/// <summary>
/// The vendor URL (optional).
/// </summary>
VendorUrl = 0x03,
/// <summary>
/// The product name (optional).
/// </summary>
ProductName = 0x04,
/// <summary>
/// The model name (optional).
/// </summary>
ModelName = 0x05,
/// <summary>
/// The application name (optional).
/// </summary>
UserApplicationName = 0x06
}
}

View File

@@ -0,0 +1,95 @@
using System.Collections.Generic;
namespace AMWD.Protocols.Modbus.Common
{
/// <summary>
/// Represents the device identification.
/// </summary>
public class DeviceIdentification
{
/// <summary>
/// Gets or sets the vendor name.
/// </summary>
/// <remarks>
/// Category: Basic
/// <br/>
/// Kind: Mandatory
/// </remarks>
public string VendorName { get; set; }
/// <summary>
/// Gets or sets the product code.
/// </summary>
/// <remarks>
/// Category: Basic
/// <br/>
/// Kind: Mandatory
/// </remarks>
public string ProductCode { get; set; }
/// <summary>
/// Gets or sets the version in major, minor and revision.
/// </summary>
/// <remarks>
/// Category: Basic
/// <br/>
/// Kind: Mandatory
/// </remarks>
public string MajorMinorRevision { get; set; }
/// <summary>
/// Gets or sets the vendor URL.
/// </summary>
/// <remarks>
/// Category: Regular
/// <br/>
/// Kind: Optional
/// </remarks>
public string VendorUrl { get; set; }
/// <summary>
/// Gets or sets the product name.
/// </summary>
/// <remarks>
/// Category: Regular
/// <br/>
/// Kind: Optional
/// </remarks>
public string ProductName { get; set; }
/// <summary>
/// Gets or sets the model name.
/// </summary>
/// <remarks>
/// Category: Regular
/// <br/>
/// Kind: Optional
/// </remarks>
public string ModelName { get; set; }
/// <summary>
/// Gets or sets the user application name.
/// </summary>
/// <remarks>
/// Category: Regular
/// <br/>
/// Kind: Optional
/// </remarks>
public string UserApplicationName { get; set; }
/// <summary>
/// Gets or sets the extended objects.
/// </summary>
/// <remarks>
/// Category: Extended
/// <br/>
/// Kind: Optional
/// </remarks>
public Dictionary<byte, byte[]> ExtendedObjects { get; set; } = [];
/// <summary>
/// Gets or sets a value indicating whether individual access (<see cref="ModbusDeviceIdentificationCategory.Individual"/>) is allowed.
/// </summary>
public bool IsIndividualAccessAllowed { get; set; }
}
}

View File

@@ -0,0 +1,30 @@
using System.Collections.Generic;
namespace AMWD.Protocols.Modbus.Common
{
/// <summary>
/// The raw device identification data as returned by the device (as spec defines).
/// </summary>
public class DeviceIdentificationRaw
{
/// <summary>
/// Gets or sets a value indicating whether the conformity level allowes an idividual access.
/// </summary>
public bool AllowsIndividualAccess { get; set; }
/// <summary>
/// Gets or sets a value indicating whether more requests are needed.
/// </summary>
public bool MoreRequestsNeeded { get; set; }
/// <summary>
/// Gets or sets the next object id to request (if <see cref="MoreRequestsNeeded"/> is <see langword="true"/>).
/// </summary>
public byte NextObjectIdToRequest { get; set; }
/// <summary>
/// Gets or sets the objects with raw bytes.
/// </summary>
public Dictionary<byte, byte[]> Objects { get; set; } = [];
}
}

View File

@@ -74,10 +74,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)
{ {
// Technically not possible to reach. Left here for completeness.
if (unitId < MIN_UNIT_ID || MAX_UNIT_ID < unitId)
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));
@@ -133,10 +129,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)
{ {
// Technically not possible to reach. Left here for completeness.
if (unitId < MIN_UNIT_ID || MAX_UNIT_ID < unitId)
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));
@@ -192,10 +184,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)
{ {
// Technically not possible to reach. Left here for completeness.
if (unitId < MIN_UNIT_ID || MAX_UNIT_ID < unitId)
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));
@@ -248,10 +236,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)
{ {
// Technically not possible to reach. Left here for completeness.
if (unitId < MIN_UNIT_ID || MAX_UNIT_ID < unitId)
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));
@@ -301,6 +285,61 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
return inputRegisters; return inputRegisters;
} }
/// <inheritdoc/>
public IReadOnlyList<byte> SerializeReadDeviceIdentification(byte unitId, ModbusDeviceIdentificationCategory category, ModbusDeviceIdentificationObject objectId)
{
if (!Enum.IsDefined(typeof(ModbusDeviceIdentificationCategory), category))
throw new ArgumentOutOfRangeException(nameof(category));
byte[] request = new byte[11];
byte[] header = GetHeader(unitId, 5);
Array.Copy(header, 0, request, 0, header.Length);
// Function code
request[7] = (byte)ModbusFunctionCode.EncapsulatedInterface;
// Modbus Encapsulated Interface: Read Device Identification (MEI Type)
request[8] = 0x0E;
// The category type (basic, regular, extended, individual)
request[9] = (byte)category;
request[10] = (byte)objectId;
return request;
}
/// <inheritdoc/>
public DeviceIdentificationRaw DeserializeReadDeviceIdentification(IReadOnlyList<byte> response)
{
if (response[8] != 0x0E)
throw new ModbusException("The MEI type does not match");
if (!Enum.IsDefined(typeof(ModbusDeviceIdentificationCategory), response[9]))
throw new ModbusException("The category type does not match");
var deviceIdentification = new DeviceIdentificationRaw
{
AllowsIndividualAccess = (response[10] & 0x80) == 0x80,
MoreRequestsNeeded = response[11] == 0xFF,
NextObjectIdToRequest = response[12],
};
int baseOffset = 14;
while (baseOffset < response.Count)
{
byte objectId = response[baseOffset];
byte length = response[baseOffset + 1];
byte[] data = response.Skip(baseOffset + 2).Take(length).ToArray();
deviceIdentification.Objects.Add(objectId, data);
baseOffset += 2 + length;
}
return deviceIdentification;
}
#endregion Read #endregion Read
#region Write #region Write
@@ -308,10 +347,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)
{ {
// Technically not possible to reach. Left here for completeness.
if (unitId < MIN_UNIT_ID || MAX_UNIT_ID < unitId)
throw new ArgumentOutOfRangeException(nameof(unitId));
#if NET8_0_OR_GREATER #if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(coil); ArgumentNullException.ThrowIfNull(coil);
#else #else
@@ -351,10 +386,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)
{ {
// Technically not possible to reach. Left here for completeness.
if (unitId < MIN_UNIT_ID || MAX_UNIT_ID < unitId)
throw new ArgumentOutOfRangeException(nameof(unitId));
#if NET8_0_OR_GREATER #if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(register); ArgumentNullException.ThrowIfNull(register);
#else #else
@@ -394,10 +425,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)
{ {
// Technically not possible to reach. Left here for completeness.
if (unitId < MIN_UNIT_ID || MAX_UNIT_ID < unitId)
throw new ArgumentOutOfRangeException(nameof(unitId));
#if NET8_0_OR_GREATER #if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(coils); ArgumentNullException.ThrowIfNull(coils);
#else #else
@@ -465,10 +492,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)
{ {
// Technically not possible to reach. Left here for completeness.
if (unitId < MIN_UNIT_ID || MAX_UNIT_ID < unitId)
throw new ArgumentOutOfRangeException(nameof(unitId));
#if NET8_0_OR_GREATER #if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(registers); ArgumentNullException.ThrowIfNull(registers);
#else #else
@@ -598,6 +621,15 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
} }
} }
/// <summary>
/// Generates the header for a Modbus request.
/// </summary>
/// <param name="unitId">The unit identifier.</param>
/// <param name="followingBytes">The number of following bytes.</param>
/// <returns>The header ready to copy to the request bytes.</returns>
/// <remarks>
/// <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) private byte[] GetHeader(byte unitId, int followingBytes)
{ {
byte[] header = new byte[7]; byte[] header = new byte[7];

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AMWD.Protocols.Modbus.Common.Contracts; using AMWD.Protocols.Modbus.Common.Contracts;
@@ -24,6 +25,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
private List<DiscreteInput> _readDiscreteInputsResponse; private List<DiscreteInput> _readDiscreteInputsResponse;
private List<HoldingRegister> _readHoldingRegistersResponse; private List<HoldingRegister> _readHoldingRegistersResponse;
private List<InputRegister> _readInputRegistersResponse; private List<InputRegister> _readInputRegistersResponse;
private DeviceIdentificationRaw _firstDeviceIdentificationResponse;
private Queue<DeviceIdentificationRaw> _deviceIdentificationResponseQueue;
private Coil _writeSingleCoilResponse; private Coil _writeSingleCoilResponse;
private HoldingRegister _writeSingleHoldingRegisterResponse; private HoldingRegister _writeSingleHoldingRegisterResponse;
private (ushort startAddress, ushort count) _writeMultipleCoilsResponse; private (ushort startAddress, ushort count) _writeMultipleCoilsResponse;
@@ -34,10 +37,10 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
{ {
_connectionIsConnectecd = true; _connectionIsConnectecd = true;
_readCoilsResponse = new List<Coil>(); _readCoilsResponse = [];
_readDiscreteInputsResponse = new List<DiscreteInput>(); _readDiscreteInputsResponse = [];
_readHoldingRegistersResponse = new List<HoldingRegister>(); _readHoldingRegistersResponse = [];
_readInputRegistersResponse = new List<InputRegister>(); _readInputRegistersResponse = [];
for (int i = 0; i < READ_COUNT; i++) for (int i = 0; i < READ_COUNT; i++)
{ {
@@ -66,6 +69,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
}); });
} }
_firstDeviceIdentificationResponse = new DeviceIdentificationRaw
{
AllowsIndividualAccess = true,
MoreRequestsNeeded = false,
NextObjectIdToRequest = 0x00,
};
_firstDeviceIdentificationResponse.Objects.Add(0x00, Encoding.ASCII.GetBytes("AM.WD"));
_firstDeviceIdentificationResponse.Objects.Add(0x01, Encoding.ASCII.GetBytes("AMWD-MB"));
_firstDeviceIdentificationResponse.Objects.Add(0x02, Encoding.ASCII.GetBytes("1.2.3"));
_firstDeviceIdentificationResponse.Objects.Add(0x03, Encoding.ASCII.GetBytes("https://github.com/AM-WD/AMWD.Protocols.Modbus"));
_firstDeviceIdentificationResponse.Objects.Add(0x04, Encoding.ASCII.GetBytes("AM.WD Modbus Library"));
_firstDeviceIdentificationResponse.Objects.Add(0x05, Encoding.ASCII.GetBytes("UnitTests"));
_firstDeviceIdentificationResponse.Objects.Add(0x06, Encoding.ASCII.GetBytes("Modbus Client Base Unit Test"));
_deviceIdentificationResponseQueue = new Queue<DeviceIdentificationRaw>();
_deviceIdentificationResponseQueue.Enqueue(_firstDeviceIdentificationResponse);
_writeSingleCoilResponse = new Coil { Address = START_ADDRESS }; _writeSingleCoilResponse = new Coil { Address = START_ADDRESS };
_writeSingleHoldingRegisterResponse = new HoldingRegister { Address = START_ADDRESS, Value = 0x1234 }; _writeSingleHoldingRegisterResponse = new HoldingRegister { Address = START_ADDRESS, Value = 0x1234 };
@@ -73,6 +93,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
_writeMultipleHoldingRegistersResponse = (START_ADDRESS, READ_COUNT); _writeMultipleHoldingRegistersResponse = (START_ADDRESS, READ_COUNT);
} }
#region Common/Connection/Assertions
[TestMethod] [TestMethod]
public void ShouldPrettyPrint() public void ShouldPrettyPrint()
{ {
@@ -210,6 +232,10 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
// Assert - ApplicationException // Assert - ApplicationException
} }
#endregion Common/Connection/Assertions
#region Read
[TestMethod] [TestMethod]
public async Task ShouldReadCoils() public async Task ShouldReadCoils()
{ {
@@ -328,6 +354,89 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
_protocol.VerifyNoOtherCalls(); _protocol.VerifyNoOtherCalls();
} }
[TestMethod]
public async Task ShouldReadDeviceIdentification()
{
// Arrange
var client = GetClient();
// Act
var result = await client.ReadDeviceIdentificationAsync(UNIT_ID, ModbusDeviceIdentificationCategory.Basic, ModbusDeviceIdentificationObject.VendorName);
// Assert
Assert.IsNotNull(result);
Assert.IsTrue(result.IsIndividualAccessAllowed);
Assert.AreEqual("AM.WD", result.VendorName);
Assert.AreEqual("AMWD-MB", result.ProductCode);
Assert.AreEqual("1.2.3", result.MajorMinorRevision);
Assert.AreEqual("https://github.com/AM-WD/AMWD.Protocols.Modbus", result.VendorUrl);
Assert.AreEqual("AM.WD Modbus Library", result.ProductName);
Assert.AreEqual("UnitTests", result.ModelName);
Assert.AreEqual("Modbus Client Base Unit Test", result.UserApplicationName);
Assert.AreEqual(0, result.ExtendedObjects.Count);
_connection.VerifyGet(c => c.IsConnected, Times.Once);
_connection.Verify(c => c.InvokeAsync(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<Func<IReadOnlyList<byte>, bool>>(), It.IsAny<CancellationToken>()), Times.Once);
_connection.VerifyNoOtherCalls();
_protocol.Verify(p => p.SerializeReadDeviceIdentification(UNIT_ID, ModbusDeviceIdentificationCategory.Basic, ModbusDeviceIdentificationObject.VendorName), Times.Once);
_protocol.Verify(p => p.ValidateResponse(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<IReadOnlyList<byte>>()), Times.Once);
_protocol.Verify(p => p.DeserializeReadDeviceIdentification(It.IsAny<IReadOnlyList<byte>>()), Times.Once);
_protocol.VerifyNoOtherCalls();
}
[TestMethod]
public async Task ShouldReadDeviceIdentificationMultipleCycles()
{
// Arrange
_firstDeviceIdentificationResponse.MoreRequestsNeeded = true;
_firstDeviceIdentificationResponse.NextObjectIdToRequest = 0x07;
_deviceIdentificationResponseQueue.Enqueue(new DeviceIdentificationRaw
{
AllowsIndividualAccess = true,
MoreRequestsNeeded = false,
NextObjectIdToRequest = 0x00,
Objects = new Dictionary<byte, byte[]>
{
{ 0x07, new byte[] { 0x01, 0x02, 0x03 } },
}
});
var client = GetClient();
// Act
var result = await client.ReadDeviceIdentificationAsync(UNIT_ID, ModbusDeviceIdentificationCategory.Extended, ModbusDeviceIdentificationObject.VendorName);
// Assert
Assert.IsNotNull(result);
Assert.IsTrue(result.IsIndividualAccessAllowed);
Assert.AreEqual("AM.WD", result.VendorName);
Assert.AreEqual("AMWD-MB", result.ProductCode);
Assert.AreEqual("1.2.3", result.MajorMinorRevision);
Assert.AreEqual("https://github.com/AM-WD/AMWD.Protocols.Modbus", result.VendorUrl);
Assert.AreEqual("AM.WD Modbus Library", result.ProductName);
Assert.AreEqual("UnitTests", result.ModelName);
Assert.AreEqual("Modbus Client Base Unit Test", result.UserApplicationName);
Assert.AreEqual(1, result.ExtendedObjects.Count);
Assert.AreEqual(0x07, result.ExtendedObjects.First().Key);
CollectionAssert.AreEqual(new byte[] { 0x01, 0x02, 0x03 }, result.ExtendedObjects.First().Value);
_connection.VerifyGet(c => c.IsConnected, Times.Once);
_connection.Verify(c => c.InvokeAsync(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<Func<IReadOnlyList<byte>, bool>>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
_connection.VerifyNoOtherCalls();
_protocol.Verify(p => p.SerializeReadDeviceIdentification(UNIT_ID, ModbusDeviceIdentificationCategory.Extended, ModbusDeviceIdentificationObject.VendorName), Times.Once);
_protocol.Verify(p => p.SerializeReadDeviceIdentification(UNIT_ID, ModbusDeviceIdentificationCategory.Extended, (ModbusDeviceIdentificationObject)0x07), Times.Once);
_protocol.Verify(p => p.ValidateResponse(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<IReadOnlyList<byte>>()), Times.Exactly(2));
_protocol.Verify(p => p.DeserializeReadDeviceIdentification(It.IsAny<IReadOnlyList<byte>>()), Times.Exactly(2));
_protocol.VerifyNoOtherCalls();
}
#endregion Read
#region Write
[TestMethod] [TestMethod]
public async Task ShouldWriteSingleCoil() public async Task ShouldWriteSingleCoil()
{ {
@@ -680,6 +789,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
_protocol.VerifyNoOtherCalls(); _protocol.VerifyNoOtherCalls();
} }
#endregion Write
private ModbusClientBase GetClient(bool disposeConnection = true) private ModbusClientBase GetClient(bool disposeConnection = true)
{ {
_connection = new Mock<IModbusConnection>(); _connection = new Mock<IModbusConnection>();
@@ -706,6 +817,9 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
_protocol _protocol
.Setup(p => p.DeserializeReadInputRegisters(It.IsAny<IReadOnlyList<byte>>())) .Setup(p => p.DeserializeReadInputRegisters(It.IsAny<IReadOnlyList<byte>>()))
.Returns(() => _readInputRegistersResponse); .Returns(() => _readInputRegistersResponse);
_protocol
.Setup(p => p.DeserializeReadDeviceIdentification(It.IsAny<IReadOnlyList<byte>>()))
.Returns(() => _deviceIdentificationResponseQueue.Dequeue());
_protocol _protocol
.Setup(p => p.DeserializeWriteSingleCoil(It.IsAny<IReadOnlyList<byte>>())) .Setup(p => p.DeserializeWriteSingleCoil(It.IsAny<IReadOnlyList<byte>>()))

View File

@@ -435,6 +435,115 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
#endregion Read Input Registers #endregion Read Input Registers
#region Read Device Identification
[DataTestMethod]
[DataRow(ModbusDeviceIdentificationCategory.Basic)]
[DataRow(ModbusDeviceIdentificationCategory.Regular)]
[DataRow(ModbusDeviceIdentificationCategory.Extended)]
[DataRow(ModbusDeviceIdentificationCategory.Individual)]
public void ShouldSerializeReadDeviceIdentification(ModbusDeviceIdentificationCategory category)
{
// Arrange
var protocol = new TcpProtocol();
// Act
var bytes = protocol.SerializeReadDeviceIdentification(UNIT_ID, category, ModbusDeviceIdentificationObject.ProductCode);
// Assert
Assert.IsNotNull(bytes);
Assert.AreEqual(11, bytes.Count);
// Transaction id
Assert.AreEqual(0x00, bytes[0]);
Assert.AreEqual(0x01, bytes[1]);
// Protocol identifier
Assert.AreEqual(0x00, bytes[2]);
Assert.AreEqual(0x00, bytes[3]);
// Following bytes
Assert.AreEqual(0x00, bytes[4]);
Assert.AreEqual(0x05, bytes[5]);
// Unit id
Assert.AreEqual(UNIT_ID, bytes[6]);
// Function code
Assert.AreEqual(0x2B, bytes[7]);
// MEI Type
Assert.AreEqual(0x0E, bytes[8]);
// Category
Assert.AreEqual((byte)category, bytes[9]);
// Object Id
Assert.AreEqual((byte)ModbusDeviceIdentificationObject.ProductCode, bytes[10]);
}
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ShouldTrhowOutOfRangeExceptionOnSerializeReadDeviceIdentification()
{
// Arrange
var protocol = new TcpProtocol();
// Act
protocol.SerializeReadDeviceIdentification(UNIT_ID, (ModbusDeviceIdentificationCategory)10, ModbusDeviceIdentificationObject.ProductCode);
// Assert - ArgumentOutOfRangeException
}
[DataTestMethod]
[DataRow(false)]
[DataRow(true)]
public void ShouldDeserializeReadDeviceIdentification(bool moreAndIndividual)
{
// Arrange
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x0D, 0x2A, 0x2B, 0x0E, 0x02, (byte)(moreAndIndividual ? 0x82 : 0x02), (byte)(moreAndIndividual ? 0xFF : 0x00), (byte)(moreAndIndividual ? 0x05 : 0x00), 0x01, 0x04, 0x02, 0x41, 0x4D];
var protocol = new TcpProtocol();
// Act
var result = protocol.DeserializeReadDeviceIdentification(response);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(moreAndIndividual, result.AllowsIndividualAccess);
Assert.AreEqual(moreAndIndividual, result.MoreRequestsNeeded);
Assert.AreEqual(moreAndIndividual ? 0x05 : 0x00, result.NextObjectIdToRequest);
Assert.AreEqual(1, result.Objects.Count);
Assert.AreEqual(4, result.Objects.First().Key);
CollectionAssert.AreEqual("AM"u8.ToArray(), result.Objects.First().Value);
}
[TestMethod]
[ExpectedException(typeof(ModbusException))]
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForMeiType()
{
// Arrange
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x0D, 0x2A, 0x2B, 0x0D];
var protocol = new TcpProtocol();
// Act
protocol.DeserializeReadDeviceIdentification(response);
}
[TestMethod]
[ExpectedException(typeof(ModbusException))]
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForCategory()
{
// Arrange
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x0D, 0x2A, 0x2B, 0x0E, 0x08];
var protocol = new TcpProtocol();
// Act
protocol.DeserializeReadDeviceIdentification(response);
}
#endregion Read Device Identification
#region Write Single Coil #region Write Single Coil
[TestMethod] [TestMethod]
@@ -1091,7 +1200,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
var protocol = new TcpProtocol(); var protocol = new TcpProtocol();
// Act // Act
var result = protocol.Name; string result = protocol.Name;
// Assert // Assert
Assert.AreEqual("TCP", result); Assert.AreEqual("TCP", result);