diff --git a/AMWD.Protocols.Modbus.Common/Contracts/IModbusProtocol.cs b/AMWD.Protocols.Modbus.Common/Contracts/IModbusProtocol.cs
index 860f18d..7d6770c 100644
--- a/AMWD.Protocols.Modbus.Common/Contracts/IModbusProtocol.cs
+++ b/AMWD.Protocols.Modbus.Common/Contracts/IModbusProtocol.cs
@@ -78,6 +78,22 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
/// A list of s.
IReadOnlyList DeserializeReadInputRegisters(IReadOnlyList response);
+ ///
+ /// Serializes a read request for device identification.
+ ///
+ /// The unit id.
+ /// The identification category to read.
+ /// The first object id to read.
+ /// The s to send.
+ IReadOnlyList SerializeReadDeviceIdentification(byte unitId, ModbusDeviceIdentificationCategory category, ModbusDeviceIdentificationObject objectId);
+
+ ///
+ /// Deserializes a read response for device identification.
+ ///
+ /// The s received.
+ /// The raw device identification information data.
+ DeviceIdentificationRaw DeserializeReadDeviceIdentification(IReadOnlyList response);
+
#endregion Read
#region Write
diff --git a/AMWD.Protocols.Modbus.Common/Contracts/ModbusClientBase.cs b/AMWD.Protocols.Modbus.Common/Contracts/ModbusClientBase.cs
index ac5d195..7abcf4f 100644
--- a/AMWD.Protocols.Modbus.Common/Contracts/ModbusClientBase.cs
+++ b/AMWD.Protocols.Modbus.Common/Contracts/ModbusClientBase.cs
@@ -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;
}
+ ///
+ /// Read the device identification.
+ ///
+ ///
+ ///
+ /// The interface consists of three (3) categories of objects:
+ ///
+ /// -
+ /// Basic Device Identification
+ /// All objects of this category are mandatory: VendorName, ProductCode and RevisionNumber.
+ ///
+ /// -
+ /// Regular Device Identification
+ /// 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.
+ ///
+ /// -
+ /// Extended Device Identification
+ /// 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.
+ ///
+ ///
+ ///
+ public virtual async Task 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;
+ }
+
///
/// Writes a single .
///
diff --git a/AMWD.Protocols.Modbus.Common/Enums/ModbusDeviceIdentificationCategory.cs b/AMWD.Protocols.Modbus.Common/Enums/ModbusDeviceIdentificationCategory.cs
new file mode 100644
index 0000000..f69f907
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Enums/ModbusDeviceIdentificationCategory.cs
@@ -0,0 +1,34 @@
+using System.ComponentModel;
+
+namespace AMWD.Protocols.Modbus.Common
+{
+ ///
+ /// List of known categories for Modbus device identification.
+ ///
+ public enum ModbusDeviceIdentificationCategory : byte
+ {
+ ///
+ /// The basic information. These are mandatory.
+ ///
+ [Description("Basic Device Identification")]
+ Basic = 0x01,
+
+ ///
+ /// The regular information. These are optional.
+ ///
+ [Description("Regular Device Identification")]
+ Regular = 0x02,
+
+ ///
+ /// The extended information. These are optional too.
+ ///
+ [Description("Extended Device Identification")]
+ Extended = 0x03,
+
+ ///
+ /// Request to a specific identification object.
+ ///
+ [Description("Request to a specific identification object")]
+ Individual = 0x04
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Enums/ModbusDeviceIdentificationObject.cs b/AMWD.Protocols.Modbus.Common/Enums/ModbusDeviceIdentificationObject.cs
new file mode 100644
index 0000000..d3d034f
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Enums/ModbusDeviceIdentificationObject.cs
@@ -0,0 +1,43 @@
+namespace AMWD.Protocols.Modbus.Common
+{
+ ///
+ /// List of known object ids for Modbus device identification.
+ ///
+ public enum ModbusDeviceIdentificationObject : byte
+ {
+ ///
+ /// The vendor name (mandatory).
+ ///
+ VendorName = 0x00,
+
+ ///
+ /// The product code (mandatory).
+ ///
+ ProductCode = 0x01,
+
+ ///
+ /// The version in major, minor and revision number (mandatory).
+ ///
+ MajorMinorRevision = 0x02,
+
+ ///
+ /// The vendor URL (optional).
+ ///
+ VendorUrl = 0x03,
+
+ ///
+ /// The product name (optional).
+ ///
+ ProductName = 0x04,
+
+ ///
+ /// The model name (optional).
+ ///
+ ModelName = 0x05,
+
+ ///
+ /// The application name (optional).
+ ///
+ UserApplicationName = 0x06
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Models/DeviceIdentification.cs b/AMWD.Protocols.Modbus.Common/Models/DeviceIdentification.cs
new file mode 100644
index 0000000..64ee898
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Models/DeviceIdentification.cs
@@ -0,0 +1,95 @@
+using System.Collections.Generic;
+
+namespace AMWD.Protocols.Modbus.Common
+{
+ ///
+ /// Represents the device identification.
+ ///
+ public class DeviceIdentification
+ {
+ ///
+ /// Gets or sets the vendor name.
+ ///
+ ///
+ /// Category: Basic
+ ///
+ /// Kind: Mandatory
+ ///
+ public string VendorName { get; set; }
+
+ ///
+ /// Gets or sets the product code.
+ ///
+ ///
+ /// Category: Basic
+ ///
+ /// Kind: Mandatory
+ ///
+ public string ProductCode { get; set; }
+
+ ///
+ /// Gets or sets the version in major, minor and revision.
+ ///
+ ///
+ /// Category: Basic
+ ///
+ /// Kind: Mandatory
+ ///
+ public string MajorMinorRevision { get; set; }
+
+ ///
+ /// Gets or sets the vendor URL.
+ ///
+ ///
+ /// Category: Regular
+ ///
+ /// Kind: Optional
+ ///
+ public string VendorUrl { get; set; }
+
+ ///
+ /// Gets or sets the product name.
+ ///
+ ///
+ /// Category: Regular
+ ///
+ /// Kind: Optional
+ ///
+ public string ProductName { get; set; }
+
+ ///
+ /// Gets or sets the model name.
+ ///
+ ///
+ /// Category: Regular
+ ///
+ /// Kind: Optional
+ ///
+ public string ModelName { get; set; }
+
+ ///
+ /// Gets or sets the user application name.
+ ///
+ ///
+ /// Category: Regular
+ ///
+ /// Kind: Optional
+ ///
+ public string UserApplicationName { get; set; }
+
+ ///
+ /// Gets or sets the extended objects.
+ ///
+ ///
+ /// Category: Extended
+ ///
+ /// Kind: Optional
+ ///
+ public Dictionary ExtendedObjects { get; set; } = [];
+
+ ///
+ /// Gets or sets a value indicating whether individual access () is allowed.
+ ///
+ public bool IsIndividualAccessAllowed { get; set; }
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Models/DeviceIdentificationRaw.cs b/AMWD.Protocols.Modbus.Common/Models/DeviceIdentificationRaw.cs
new file mode 100644
index 0000000..e135607
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Models/DeviceIdentificationRaw.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+
+namespace AMWD.Protocols.Modbus.Common
+{
+ ///
+ /// The raw device identification data as returned by the device (as spec defines).
+ ///
+ public class DeviceIdentificationRaw
+ {
+ ///
+ /// Gets or sets a value indicating whether the conformity level allowes an idividual access.
+ ///
+ public bool AllowsIndividualAccess { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether more requests are needed.
+ ///
+ public bool MoreRequestsNeeded { get; set; }
+
+ ///
+ /// Gets or sets the next object id to request (if is ).
+ ///
+ public byte NextObjectIdToRequest { get; set; }
+
+ ///
+ /// Gets or sets the objects with raw bytes.
+ ///
+ public Dictionary Objects { get; set; } = [];
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs b/AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs
index 91ab546..0107cc7 100644
--- a/AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs
+++ b/AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs
@@ -74,10 +74,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
///
public IReadOnlyList 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
///
public IReadOnlyList 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
///
public IReadOnlyList 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
///
public IReadOnlyList 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;
}
+ ///
+ public IReadOnlyList 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;
+ }
+
+ ///
+ public DeviceIdentificationRaw DeserializeReadDeviceIdentification(IReadOnlyList 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
///
public IReadOnlyList 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
///
public IReadOnlyList 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
///
public IReadOnlyList SerializeWriteMultipleCoils(byte unitId, IReadOnlyList 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
///
public IReadOnlyList SerializeWriteMultipleHoldingRegisters(byte unitId, IReadOnlyList 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
}
}
+ ///
+ /// Generates the header for a Modbus request.
+ ///
+ /// The unit identifier.
+ /// The number of following bytes.
+ /// The header ready to copy to the request bytes.
+ ///
+ /// ATTENTION: Do not forget the . It is placed after the count information.
+ ///
private byte[] GetHeader(byte unitId, int followingBytes)
{
byte[] header = new byte[7];
diff --git a/AMWD.Protocols.Modbus.Tests/Common/Contracts/ModbusClientBaseTest.cs b/AMWD.Protocols.Modbus.Tests/Common/Contracts/ModbusClientBaseTest.cs
index 8ee01c6..1f22f2b 100644
--- a/AMWD.Protocols.Modbus.Tests/Common/Contracts/ModbusClientBaseTest.cs
+++ b/AMWD.Protocols.Modbus.Tests/Common/Contracts/ModbusClientBaseTest.cs
@@ -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 _readDiscreteInputsResponse;
private List _readHoldingRegistersResponse;
private List _readInputRegistersResponse;
+ private DeviceIdentificationRaw _firstDeviceIdentificationResponse;
+ private Queue _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();
- _readDiscreteInputsResponse = new List();
- _readHoldingRegistersResponse = new List();
- _readInputRegistersResponse = new List();
+ _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();
+ _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>(), It.IsAny, bool>>(), It.IsAny()), Times.Once);
+ _connection.VerifyNoOtherCalls();
+
+ _protocol.Verify(p => p.SerializeReadDeviceIdentification(UNIT_ID, ModbusDeviceIdentificationCategory.Basic, ModbusDeviceIdentificationObject.VendorName), Times.Once);
+ _protocol.Verify(p => p.ValidateResponse(It.IsAny>(), It.IsAny>()), Times.Once);
+ _protocol.Verify(p => p.DeserializeReadDeviceIdentification(It.IsAny>()), 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
+ {
+ { 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>(), It.IsAny, bool>>(), It.IsAny()), 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>(), It.IsAny>()), Times.Exactly(2));
+ _protocol.Verify(p => p.DeserializeReadDeviceIdentification(It.IsAny>()), 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();
@@ -706,6 +817,9 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
_protocol
.Setup(p => p.DeserializeReadInputRegisters(It.IsAny>()))
.Returns(() => _readInputRegistersResponse);
+ _protocol
+ .Setup(p => p.DeserializeReadDeviceIdentification(It.IsAny>()))
+ .Returns(() => _deviceIdentificationResponseQueue.Dequeue());
_protocol
.Setup(p => p.DeserializeWriteSingleCoil(It.IsAny>()))
diff --git a/AMWD.Protocols.Modbus.Tests/Common/Protocols/TcpProtocolTest.cs b/AMWD.Protocols.Modbus.Tests/Common/Protocols/TcpProtocolTest.cs
index 253c976..4c8d7b9 100644
--- a/AMWD.Protocols.Modbus.Tests/Common/Protocols/TcpProtocolTest.cs
+++ b/AMWD.Protocols.Modbus.Tests/Common/Protocols/TcpProtocolTest.cs
@@ -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);