using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Threading; using System.Linq; 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 a value indicating whether the client is connected. /// public bool IsConnected => connection.IsConnected; /// /// Gets or sets the protocol type to use. /// /// /// The default protocol used by the client should be initialized in the constructor. /// public abstract IModbusProtocol Protocol { get; set; } /// /// Starts the connection to the remote endpoint. /// /// A cancellation token used to propagate notification that this operation should be canceled. /// An awaitable . public virtual Task ConnectAsync(CancellationToken cancellationToken = default) { Assertions(false); return connection.ConnectAsync(cancellationToken); } /// /// Stops the connection to the remote endpoint. /// /// A cancellation token used to propagate notification that this operation should be canceled. /// An awaitable . public virtual Task DisconnectAsync(CancellationToken cancellationToken = default) { Assertions(false); return connection.DisconnectAsync(cancellationToken); } /// /// 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; } /// /// 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(bool checkConnected = true) { #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 if (!checkConnected) return; if (!IsConnected) throw new ApplicationException($"Connection is not open"); } } }