Implemented Device Identification (0x2B / 43) for TCP
This commit is contained in:
@@ -78,6 +78,22 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
/// <returns>A list of <see cref="InputRegister"/>s.</returns>
|
||||
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
|
||||
|
||||
#region Write
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
{
|
||||
@@ -176,6 +177,91 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
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>
|
||||
/// Writes a single <see cref="Coil"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
95
AMWD.Protocols.Modbus.Common/Models/DeviceIdentification.cs
Normal file
95
AMWD.Protocols.Modbus.Common/Models/DeviceIdentification.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
}
|
||||
@@ -74,10 +74,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
/// <inheritdoc/>
|
||||
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)
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
|
||||
@@ -133,10 +129,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
/// <inheritdoc/>
|
||||
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)
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
|
||||
@@ -192,10 +184,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
/// <inheritdoc/>
|
||||
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)
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
|
||||
@@ -248,10 +236,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
/// <inheritdoc/>
|
||||
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)
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
|
||||
@@ -301,6 +285,61 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
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
|
||||
|
||||
#region Write
|
||||
@@ -308,10 +347,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
ArgumentNullException.ThrowIfNull(coil);
|
||||
#else
|
||||
@@ -351,10 +386,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
ArgumentNullException.ThrowIfNull(register);
|
||||
#else
|
||||
@@ -394,10 +425,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
ArgumentNullException.ThrowIfNull(coils);
|
||||
#else
|
||||
@@ -465,10 +492,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
ArgumentNullException.ThrowIfNull(registers);
|
||||
#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)
|
||||
{
|
||||
byte[] header = new byte[7];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||
@@ -24,6 +25,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
|
||||
private List<DiscreteInput> _readDiscreteInputsResponse;
|
||||
private List<HoldingRegister> _readHoldingRegistersResponse;
|
||||
private List<InputRegister> _readInputRegistersResponse;
|
||||
private DeviceIdentificationRaw _firstDeviceIdentificationResponse;
|
||||
private Queue<DeviceIdentificationRaw> _deviceIdentificationResponseQueue;
|
||||
private Coil _writeSingleCoilResponse;
|
||||
private HoldingRegister _writeSingleHoldingRegisterResponse;
|
||||
private (ushort startAddress, ushort count) _writeMultipleCoilsResponse;
|
||||
@@ -34,10 +37,10 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
|
||||
{
|
||||
_connectionIsConnectecd = true;
|
||||
|
||||
_readCoilsResponse = new List<Coil>();
|
||||
_readDiscreteInputsResponse = new List<DiscreteInput>();
|
||||
_readHoldingRegistersResponse = new List<HoldingRegister>();
|
||||
_readInputRegistersResponse = new List<InputRegister>();
|
||||
_readCoilsResponse = [];
|
||||
_readDiscreteInputsResponse = [];
|
||||
_readHoldingRegistersResponse = [];
|
||||
_readInputRegistersResponse = [];
|
||||
|
||||
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 };
|
||||
_writeSingleHoldingRegisterResponse = new HoldingRegister { Address = START_ADDRESS, Value = 0x1234 };
|
||||
|
||||
@@ -73,6 +93,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
|
||||
_writeMultipleHoldingRegistersResponse = (START_ADDRESS, READ_COUNT);
|
||||
}
|
||||
|
||||
#region Common/Connection/Assertions
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldPrettyPrint()
|
||||
{
|
||||
@@ -210,6 +232,10 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
|
||||
// Assert - ApplicationException
|
||||
}
|
||||
|
||||
#endregion Common/Connection/Assertions
|
||||
|
||||
#region Read
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldReadCoils()
|
||||
{
|
||||
@@ -328,6 +354,89 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
|
||||
_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]
|
||||
public async Task ShouldWriteSingleCoil()
|
||||
{
|
||||
@@ -680,6 +789,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
#endregion Write
|
||||
|
||||
private ModbusClientBase GetClient(bool disposeConnection = true)
|
||||
{
|
||||
_connection = new Mock<IModbusConnection>();
|
||||
@@ -706,6 +817,9 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
|
||||
_protocol
|
||||
.Setup(p => p.DeserializeReadInputRegisters(It.IsAny<IReadOnlyList<byte>>()))
|
||||
.Returns(() => _readInputRegistersResponse);
|
||||
_protocol
|
||||
.Setup(p => p.DeserializeReadDeviceIdentification(It.IsAny<IReadOnlyList<byte>>()))
|
||||
.Returns(() => _deviceIdentificationResponseQueue.Dequeue());
|
||||
|
||||
_protocol
|
||||
.Setup(p => p.DeserializeWriteSingleCoil(It.IsAny<IReadOnlyList<byte>>()))
|
||||
|
||||
@@ -435,6 +435,115 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
|
||||
#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
|
||||
|
||||
[TestMethod]
|
||||
@@ -1091,7 +1200,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
var result = protocol.Name;
|
||||
string result = protocol.Name;
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("TCP", result);
|
||||
|
||||
Reference in New Issue
Block a user