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

View File

@@ -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>

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/>
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];