using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
using System.Linq;
using System.Text;
namespace AMWD.Protocols.Modbus.Common.Contracts
{
///
/// Base implementation of a Modbus client.
///
public abstract class ModbusClientBase : IDisposable
{
private bool _isDisposed;
///
/// Gets or sets a value indicating whether the connection should be disposed of by .
///
protected readonly bool disposeConnection;
///
/// Gets or sets the responsible for invoking the requests.
///
protected readonly IModbusConnection connection;
///
/// Initializes a new instance of the class with a specific .
///
/// The responsible for invoking the requests.
public ModbusClientBase(IModbusConnection connection)
: this(connection, true)
{ }
///
/// Initializes a new instance of the class with a specific .
///
/// The responsible for invoking the requests.
///
/// if the connection should be disposed of by Dispose(),
/// otherwise if you inted to reuse the connection.
///
public ModbusClientBase(IModbusConnection connection, bool disposeConnection)
{
this.connection = connection ?? throw new ArgumentNullException(nameof(connection));
this.disposeConnection = disposeConnection;
}
///
/// Gets or sets the protocol type to use.
///
///
/// The default protocol used by the client should be initialized in the constructor.
///
public virtual IModbusProtocol Protocol { get; set; }
///
/// Reads multiple s.
///
/// The unit id.
/// The starting address.
/// The number of coils to read.
/// A cancellation token used to propagate notification that this operation should be canceled.
/// A list of s.
public virtual async Task> ReadCoilsAsync(byte unitId, ushort startAddress, ushort count, CancellationToken cancellationToken = default)
{
Assertions();
var request = Protocol.SerializeReadCoils(unitId, startAddress, count);
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
Protocol.ValidateResponse(request, response);
// The protocol processes complete bytes from the response.
// So reduce to the actual coil count.
var coils = Protocol.DeserializeReadCoils(response).Take(count);
foreach (var coil in coils)
coil.Address += startAddress;
return coils.ToList();
}
///
/// Reads multiple s.
///
/// The unit id.
/// The starting address.
/// The number of inputs to read.
/// A cancellation token used to propagate notification that this operation should be canceled.
/// A list of s.
public virtual async Task> ReadDiscreteInputsAsync(byte unitId, ushort startAddress, ushort count, CancellationToken cancellationToken = default)
{
Assertions();
var request = Protocol.SerializeReadDiscreteInputs(unitId, startAddress, count);
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
Protocol.ValidateResponse(request, response);
// The protocol processes complete bytes from the response.
// So reduce to the actual discrete input count.
var discreteInputs = Protocol.DeserializeReadDiscreteInputs(response).Take(count);
foreach (var discreteInput in discreteInputs)
discreteInput.Address += startAddress;
return discreteInputs.ToList();
}
///
/// Reads multiple s.
///
/// The unit id.
/// The starting address.
/// The number of registers to read.
/// A cancellation token used to propagate notification that this operation should be canceled.
/// A list of s.
public virtual async Task> ReadHoldingRegistersAsync(byte unitId, ushort startAddress, ushort count, CancellationToken cancellationToken = default)
{
Assertions();
var request = Protocol.SerializeReadHoldingRegisters(unitId, startAddress, count);
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
Protocol.ValidateResponse(request, response);
var holdingRegisters = Protocol.DeserializeReadHoldingRegisters(response).ToList();
foreach (var holdingRegister in holdingRegisters)
holdingRegister.Address += startAddress;
return holdingRegisters;
}
///
/// Reads multiple s.
///
/// The unit id.
/// The starting address.
/// The number of registers to read.
/// A cancellation token used to propagate notification that this operation should be canceled.
/// A list of s.
public virtual async Task> ReadInputRegistersAsync(byte unitId, ushort startAddress, ushort count, CancellationToken cancellationToken = default)
{
Assertions();
var request = Protocol.SerializeReadInputRegisters(unitId, startAddress, count);
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
Protocol.ValidateResponse(request, response);
var inputRegisters = Protocol.DeserializeReadInputRegisters(response).ToList();
foreach (var inputRegister in inputRegisters)
inputRegister.Address += startAddress;
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.UTF8.GetString(item.Value);
break;
case ModbusDeviceIdentificationObject.ProductCode:
devIdent.ProductCode = Encoding.UTF8.GetString(item.Value);
break;
case ModbusDeviceIdentificationObject.MajorMinorRevision:
devIdent.MajorMinorRevision = Encoding.UTF8.GetString(item.Value);
break;
case ModbusDeviceIdentificationObject.VendorUrl:
devIdent.VendorUrl = Encoding.UTF8.GetString(item.Value);
break;
case ModbusDeviceIdentificationObject.ProductName:
devIdent.ProductName = Encoding.UTF8.GetString(item.Value);
break;
case ModbusDeviceIdentificationObject.ModelName:
devIdent.ModelName = Encoding.UTF8.GetString(item.Value);
break;
case ModbusDeviceIdentificationObject.UserApplicationName:
devIdent.UserApplicationName = Encoding.UTF8.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 .
///
/// The unit id.
/// The coil to write.
/// A cancellation token used to propagate notification that this operation should be canceled.
/// on success, otherwise .
public virtual async Task WriteSingleCoilAsync(byte unitId, Coil coil, CancellationToken cancellationToken = default)
{
Assertions();
var request = Protocol.SerializeWriteSingleCoil(unitId, coil);
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
Protocol.ValidateResponse(request, response);
var result = Protocol.DeserializeWriteSingleCoil(response);
return coil.Address == result.Address
&& coil.Value == result.Value;
}
///
/// Writs a single .
///
/// The unit id.
/// The register to write.
/// A cancellation token used to propagate notification that this operation should be canceled.
/// on success, otherwise .
public virtual async Task WriteSingleHoldingRegisterAsync(byte unitId, HoldingRegister register, CancellationToken cancellationToken = default)
{
Assertions();
var request = Protocol.SerializeWriteSingleHoldingRegister(unitId, register);
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
Protocol.ValidateResponse(request, response);
var result = Protocol.DeserializeWriteSingleHoldingRegister(response);
return register.Address == result.Address
&& register.Value == result.Value;
}
///
/// Writes multiple s.
///
/// The unit id.
/// The coils to write.
/// A cancellation token used to propagate notification that this operation should be canceled.
/// on success, otherwise .
public virtual async Task WriteMultipleCoilsAsync(byte unitId, IReadOnlyList coils, CancellationToken cancellationToken = default)
{
Assertions();
var request = Protocol.SerializeWriteMultipleCoils(unitId, coils);
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
Protocol.ValidateResponse(request, response);
var (firstAddress, count) = Protocol.DeserializeWriteMultipleCoils(response);
return coils.Count == count && coils.OrderBy(c => c.Address).First().Address == firstAddress;
}
///
/// Writes multiple s.
///
/// The unit id.
/// The registers to write.
/// A cancellation token used to propagate notification that this operation should be canceled.
/// on success, otherwise .
public virtual async Task WriteMultipleHoldingRegistersAsync(byte unitId, IReadOnlyList registers, CancellationToken cancellationToken = default)
{
Assertions();
var request = Protocol.SerializeWriteMultipleHoldingRegisters(unitId, registers);
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
Protocol.ValidateResponse(request, response);
var (firstAddress, count) = Protocol.DeserializeWriteMultipleHoldingRegisters(response);
return registers.Count == count && registers.OrderBy(c => c.Address).First().Address == firstAddress;
}
///
/// Releases all managed and unmanaged resources used by the .
///
public virtual void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
public override string ToString()
=> $"Modbus client using {Protocol.Name} protocol to connect via {connection.Name}";
///
/// Releases the unmanaged resources used by the
/// and optionally also discards the managed resources.
///
protected virtual void Dispose(bool disposing)
{
if (disposing && !_isDisposed)
{
_isDisposed = true;
if (disposeConnection)
connection.Dispose();
}
}
///
/// Performs basic assertions.
///
protected virtual void Assertions()
{
#if NET8_0_OR_GREATER
ObjectDisposedException.ThrowIf(_isDisposed, this);
#else
if (_isDisposed)
throw new ObjectDisposedException(GetType().FullName);
#endif
#if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(Protocol);
#else
if (Protocol == null)
throw new ArgumentNullException(nameof(Protocol));
#endif
}
}
}