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