Implementation of the basic functionallity
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
|
||||
<AssemblyName>amwd-modbus-common</AssemblyName>
|
||||
<RootNamespace>AMWD.Protocols.Modbus.Common</RootNamespace>
|
||||
|
||||
<Product>Modbus Protocol Common</Product>
|
||||
<Description>Common data for Modbus protocol.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
46
AMWD.Protocols.Modbus.Common/Contracts/IModbusConnection.cs
Normal file
46
AMWD.Protocols.Modbus.Common/Contracts/IModbusConnection.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a Modbus connection.
|
||||
/// </summary>
|
||||
public interface IModbusConnection : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The connection type name.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the connection is open.
|
||||
/// </summary>
|
||||
bool IsConnected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Opens the connection to the remote device.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns>An awaitable <see cref="Task"/>.</returns>
|
||||
Task ConnectAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Closes the connection to the remote device.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns>An awaitable <see cref="Task"/>.</returns>
|
||||
Task DisconnectAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes a Modbus request.
|
||||
/// </summary>
|
||||
/// <param name="request">The Modbus request serialized in bytes.</param>
|
||||
/// <param name="validateResponseComplete">A function to validate whether the response is complete.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns>A list of <see cref="byte"/>s containing the response.</returns>
|
||||
Task<IReadOnlyList<byte>> InvokeAsync(IReadOnlyList<byte> request, Func<IReadOnlyList<byte>, bool> validateResponseComplete, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
165
AMWD.Protocols.Modbus.Common/Contracts/IModbusProtocol.cs
Normal file
165
AMWD.Protocols.Modbus.Common/Contracts/IModbusProtocol.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// A definition of the capabilities an implementation of the Modbus protocol version should have.
|
||||
/// </summary>
|
||||
public interface IModbusProtocol
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the protocol type name.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
#region Read
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a read request for <see cref="Coil"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="startAddress">The starting address.</param>
|
||||
/// <param name="count">The number of coils to read.</param>
|
||||
/// <returns>The <see langword="byte"/>s to send.</returns>
|
||||
IReadOnlyList<byte> SerializeReadCoils(byte unitId, ushort startAddress, ushort count);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a read response for <see cref="Coil"/>s.
|
||||
/// </summary>
|
||||
/// <param name="response">The <see langword="byte"/>s received.</param>
|
||||
/// <returns>A list of <see cref="Coil"/>s.</returns>
|
||||
IReadOnlyList<Coil> DeserializeReadCoils(IReadOnlyList<byte> response);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a read request for <see cref="DiscreteInput"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="startAddress">The starting address.</param>
|
||||
/// <param name="count">The number of discrete inputs to read.</param>
|
||||
/// <returns>The <see langword="byte"/>s to send.</returns>
|
||||
IReadOnlyList<byte> SerializeReadDiscreteInputs(byte unitId, ushort startAddress, ushort count);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a read response for <see cref="DiscreteInput"/>s.
|
||||
/// </summary>
|
||||
/// <param name="response">The <see langword="byte"/>s received.</param>
|
||||
/// <returns>A list of <see cref="DiscreteInput"/>s.</returns>
|
||||
IReadOnlyList<DiscreteInput> DeserializeReadDiscreteInputs(IReadOnlyList<byte> response);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a read request for <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="startAddress">The starting address.</param>
|
||||
/// <param name="count">The number of holding registers to read.</param>
|
||||
/// <returns>The <see langword="byte"/>s to send.</returns>
|
||||
IReadOnlyList<byte> SerializeReadHoldingRegisters(byte unitId, ushort startAddress, ushort count);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a read response for <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="response">The <see langword="byte"/>s received.</param>
|
||||
/// <returns>A list of <see cref="HoldingRegister"/>s.</returns>
|
||||
IReadOnlyList<HoldingRegister> DeserializeReadHoldingRegisters(IReadOnlyList<byte> response);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a read request for <see cref="InputRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="startAddress">The starting address.</param>
|
||||
/// <param name="count">The number of input registers to read.</param>
|
||||
/// <returns>The <see langword="byte"/>s to send.</returns>
|
||||
IReadOnlyList<byte> SerializeReadInputRegisters(byte unitId, ushort startAddress, ushort count);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a read response for <see cref="InputRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="response">The <see langword="byte"/>s received.</param>
|
||||
/// <returns>A list of <see cref="InputRegister"/>s.</returns>
|
||||
IReadOnlyList<InputRegister> DeserializeReadInputRegisters(IReadOnlyList<byte> response);
|
||||
|
||||
#endregion Read
|
||||
|
||||
#region Write
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a write request for a single <see cref="Coil"/>.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="coil">The coil to write.</param>
|
||||
/// <returns>The <see langword="byte"/>s to send.</returns>
|
||||
IReadOnlyList<byte> SerializeWriteSingleCoil(byte unitId, Coil coil);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a write response for a single <see cref="Coil"/>.
|
||||
/// </summary>
|
||||
/// <param name="response">The <see langword="byte"/>s received.</param>
|
||||
/// <returns>Should be the coil itself, as the response is an echo of the request.</returns>
|
||||
Coil DeserializeWriteSingleCoil(IReadOnlyList<byte> response);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a write request for a single <see cref="HoldingRegister"/>.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="register">The holding register to write.</param>
|
||||
/// <returns>The <see langword="byte"/>s to send.</returns>
|
||||
IReadOnlyList<byte> SerializeWriteSingleHoldingRegister(byte unitId, HoldingRegister register);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a write response for a single <see cref="HoldingRegister"/>.
|
||||
/// </summary>
|
||||
/// <param name="response">The <see langword="byte"/>s received.</param>
|
||||
/// <returns>Should be the holding register itself, as the response is an echo of the request.</returns>
|
||||
HoldingRegister DeserializeWriteSingleHoldingRegister(IReadOnlyList<byte> response);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a write request for multiple <see cref="Coil"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="coils">The coils to write.</param>
|
||||
/// <returns>The <see langword="byte"/>s to send.</returns>
|
||||
IReadOnlyList<byte> SerializeWriteMultipleCoils(byte unitId, IReadOnlyList<Coil> coils);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a write response for multiple <see cref="Coil"/>s.
|
||||
/// </summary>
|
||||
/// <param name="response">The <see langword="byte"/>s received.</param>
|
||||
/// <returns>A tuple containting the first address and the number of coils written.</returns>
|
||||
(ushort FirstAddress, ushort NumberOfCoils) DeserializeWriteMultipleCoils(IReadOnlyList<byte> response);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a write request for multiple <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="registers">The holding registers to write.</param>
|
||||
/// <returns>The <see langword="byte"/>s to send.</returns>
|
||||
IReadOnlyList<byte> SerializeWriteMultipleHoldingRegisters(byte unitId, IReadOnlyList<HoldingRegister> registers);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a write response for multiple <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="response">The <see langword="byte"/>s received.</param>
|
||||
/// <returns>A tuple containting the first address and the number of holding registers written.</returns>
|
||||
(ushort FirstAddress, ushort NumberOfRegisters) DeserializeWriteMultipleHoldingRegisters(IReadOnlyList<byte> response);
|
||||
|
||||
#endregion Write
|
||||
|
||||
#region Control
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the receive response bytes are complete to deserialize the response.
|
||||
/// </summary>
|
||||
/// <param name="responseBytes">The already received response bytes.</param>
|
||||
/// <returns><see langword="true"/> when the response is complete, otherwise <see langword="false"/>.</returns>
|
||||
bool CheckResponseComplete(IReadOnlyList<byte> responseBytes);
|
||||
|
||||
/// <summary>
|
||||
/// Validates the response against the request and throws <see cref="ModbusException"/>s if necessary.
|
||||
/// </summary>
|
||||
/// <param name="request">The serialized request.</param>
|
||||
/// <param name="response">The received response.</param>
|
||||
void ValidateResponse(IReadOnlyList<byte> request, IReadOnlyList<byte> response);
|
||||
|
||||
#endregion Control
|
||||
}
|
||||
}
|
||||
315
AMWD.Protocols.Modbus.Common/Contracts/ModbusClientBase.cs
Normal file
315
AMWD.Protocols.Modbus.Common/Contracts/ModbusClientBase.cs
Normal file
@@ -0,0 +1,315 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Linq;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Base implementation of a Modbus client.
|
||||
/// </summary>
|
||||
public abstract class ModbusClientBase : IDisposable
|
||||
{
|
||||
private bool _isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the connection should be disposed of by <see cref="Dispose()"/>.
|
||||
/// </summary>
|
||||
protected readonly bool disposeConnection;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="IModbusConnection"/> responsible for invoking the requests.
|
||||
/// </summary>
|
||||
protected readonly IModbusConnection connection;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModbusClientBase"/> class with a specific <see cref="IModbusConnection"/>.
|
||||
/// </summary>
|
||||
/// <param name="connection">The <see cref="IModbusConnection"/> responsible for invoking the requests.</param>
|
||||
public ModbusClientBase(IModbusConnection connection)
|
||||
: this(connection, true)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModbusClientBase"/> class with a specific <see cref="IModbusConnection"/>.
|
||||
/// </summary>
|
||||
/// <param name="connection">The <see cref="IModbusConnection"/> responsible for invoking the requests.</param>
|
||||
/// <param name="disposeConnection">
|
||||
/// <see langword="true"/> if the connection should be disposed of by Dispose(),
|
||||
/// <see langword="false"/> otherwise if you inted to reuse the connection.
|
||||
/// </param>
|
||||
public ModbusClientBase(IModbusConnection connection, bool disposeConnection)
|
||||
{
|
||||
this.connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||
this.disposeConnection = disposeConnection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the client is connected.
|
||||
/// </summary>
|
||||
public bool IsConnected => connection.IsConnected;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the protocol type to use.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The default protocol used by the client should be initialized in the constructor.
|
||||
/// </remarks>
|
||||
public abstract IModbusProtocol Protocol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Starts the connection to the remote endpoint.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns>An awaitable <see cref="Task"/>.</returns>
|
||||
public virtual Task ConnectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Assertions(false);
|
||||
return connection.ConnectAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the connection to the remote endpoint.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns>An awaitable <see cref="Task"/>.</returns>
|
||||
public virtual Task DisconnectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Assertions(false);
|
||||
return connection.DisconnectAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads multiple <see cref="Coil"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="startAddress">The starting address.</param>
|
||||
/// <param name="count">The number of coils to read.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns>A list of <see cref="Coil"/>s.</returns>
|
||||
public virtual async Task<IReadOnlyList<Coil>> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads multiple <see cref="DiscreteInput"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="startAddress">The starting address.</param>
|
||||
/// <param name="count">The number of inputs to read.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns>A list of <see cref="DiscreteInput"/>s.</returns>
|
||||
public virtual async Task<IReadOnlyList<DiscreteInput>> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads multiple <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="startAddress">The starting address.</param>
|
||||
/// <param name="count">The number of registers to read.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns>A list of <see cref="HoldingRegister"/>s.</returns>
|
||||
public virtual async Task<IReadOnlyList<HoldingRegister>> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads multiple <see cref="InputRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="startAddress">The starting address.</param>
|
||||
/// <param name="count">The number of registers to read.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns>A list of <see cref="InputRegister"/>s.</returns>
|
||||
public virtual async Task<IReadOnlyList<InputRegister>> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a single <see cref="Coil"/>.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="coil">The coil to write.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns><see langword="true"/> on success, otherwise <see langword="false"/>.</returns>
|
||||
public virtual async Task<bool> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writs a single <see cref="HoldingRegister"/>.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="register">The register to write.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns><see langword="true"/> on success, otherwise <see langword="false"/>.</returns>
|
||||
public virtual async Task<bool> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes multiple <see cref="Coil"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="coils">The coils to write.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns><see langword="true"/> on success, otherwise <see langword="false"/>.</returns>
|
||||
public virtual async Task<bool> WriteMultipleCoilsAsync(byte unitId, IReadOnlyList<Coil> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes multiple <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="registers">The registers to write.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns><see langword="true"/> on success, otherwise <see langword="false"/>.</returns>
|
||||
public virtual async Task<bool> WriteMultipleHoldingRegistersAsync(byte unitId, IReadOnlyList<HoldingRegister> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases all managed and unmanaged resources used by the <see cref="ModbusClientBase"/>.
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
=> $"Modbus client using {Protocol.Name} protocol to connect via {connection.Name}";
|
||||
|
||||
/// <summary>
|
||||
/// Releases the unmanaged resources used by the <see cref="ModbusClientBase"/>
|
||||
/// and optionally also discards the managed resources.
|
||||
/// </summary>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && !_isDisposed)
|
||||
{
|
||||
_isDisposed = true;
|
||||
|
||||
if (disposeConnection)
|
||||
connection.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs basic assertions.
|
||||
/// </summary>
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
76
AMWD.Protocols.Modbus.Common/Enums/ModbusErrorCode.cs
Normal file
76
AMWD.Protocols.Modbus.Common/Enums/ModbusErrorCode.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// List of Modbus exception codes.
|
||||
/// </summary>
|
||||
public enum ModbusErrorCode : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// No error.
|
||||
/// </summary>
|
||||
[Description("No error")]
|
||||
NoError = 0x00,
|
||||
|
||||
/// <summary>
|
||||
/// Function code not valid/supported.
|
||||
/// </summary>
|
||||
[Description("Illegal function")]
|
||||
IllegalFunction = 0x01,
|
||||
|
||||
/// <summary>
|
||||
/// Data address not in range.
|
||||
/// </summary>
|
||||
[Description("Illegal data address")]
|
||||
IllegalDataAddress = 0x02,
|
||||
|
||||
/// <summary>
|
||||
/// The data value to set is not valid.
|
||||
/// </summary>
|
||||
[Description("Illegal data value")]
|
||||
IllegalDataValue = 0x03,
|
||||
|
||||
/// <summary>
|
||||
/// Slave device produced a failure.
|
||||
/// </summary>
|
||||
[Description("Slave device failure")]
|
||||
SlaveDeviceFailure = 0x04,
|
||||
|
||||
/// <summary>
|
||||
/// Ack
|
||||
/// </summary>
|
||||
[Description("Acknowledge")]
|
||||
Acknowledge = 0x05,
|
||||
|
||||
/// <summary>
|
||||
/// Slave device is working on another task.
|
||||
/// </summary>
|
||||
[Description("Slave device busy")]
|
||||
SlaveDeviceBusy = 0x06,
|
||||
|
||||
/// <summary>
|
||||
/// nAck
|
||||
/// </summary>
|
||||
[Description("Negative acknowledge")]
|
||||
NegativeAcknowledge = 0x07,
|
||||
|
||||
/// <summary>
|
||||
/// Momory Parity Error.
|
||||
/// </summary>
|
||||
[Description("Memory parity error")]
|
||||
MemoryParityError = 0x08,
|
||||
|
||||
/// <summary>
|
||||
/// Gateway of the device could not be reached.
|
||||
/// </summary>
|
||||
[Description("Gateway path unavailable")]
|
||||
GatewayPath = 0x0A,
|
||||
|
||||
/// <summary>
|
||||
/// Gateway device did no respond.
|
||||
/// </summary>
|
||||
[Description("Gateway target device failed to respond")]
|
||||
GatewayTargetDevice = 0x0B
|
||||
}
|
||||
}
|
||||
67
AMWD.Protocols.Modbus.Common/Enums/ModbusFunctionCode.cs
Normal file
67
AMWD.Protocols.Modbus.Common/Enums/ModbusFunctionCode.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// List of the Modbus function codes.
|
||||
/// </summary>
|
||||
public enum ModbusFunctionCode : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Read coils (Fn 1).
|
||||
/// </summary>
|
||||
[Description("Read Coils")]
|
||||
ReadCoils = 0x01,
|
||||
|
||||
/// <summary>
|
||||
/// Read discrete inputs (Fn 2).
|
||||
/// </summary>
|
||||
[Description("Read Discrete Inputs")]
|
||||
ReadDiscreteInputs = 0x02,
|
||||
|
||||
/// <summary>
|
||||
/// Reads holding registers (Fn 3).
|
||||
/// </summary>
|
||||
[Description("Read Holding Registers")]
|
||||
ReadHoldingRegisters = 0x03,
|
||||
|
||||
/// <summary>
|
||||
/// Reads input registers (Fn 4).
|
||||
/// </summary>
|
||||
[Description("Read Input Registers")]
|
||||
ReadInputRegisters = 0x04,
|
||||
|
||||
/// <summary>
|
||||
/// Writes a single coil (Fn 5).
|
||||
/// </summary>
|
||||
[Description("Write Single Coil")]
|
||||
WriteSingleCoil = 0x05,
|
||||
|
||||
/// <summary>
|
||||
/// Writes a single register (Fn 6).
|
||||
/// </summary>
|
||||
[Description("Write Single Register")]
|
||||
WriteSingleRegister = 0x06,
|
||||
|
||||
/// <summary>
|
||||
/// Writes multiple coils (Fn 15).
|
||||
/// </summary>
|
||||
[Description("Write Multiple Coils")]
|
||||
WriteMultipleCoils = 0x0F,
|
||||
|
||||
/// <summary>
|
||||
/// Writes multiple registers (Fn 16).
|
||||
/// </summary>
|
||||
[Description("Write Multiple Registers")]
|
||||
WriteMultipleRegisters = 0x10,
|
||||
|
||||
/// <summary>
|
||||
/// Tunnels service requests and method invocations (Fn 43).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This function code needs additional information about its type of request.
|
||||
/// </remarks>
|
||||
[Description("MODBUS Encapsulated Interface (MEI)")]
|
||||
EncapsulatedInterface = 0x2B
|
||||
}
|
||||
}
|
||||
28
AMWD.Protocols.Modbus.Common/Enums/ModbusObjectType.cs
Normal file
28
AMWD.Protocols.Modbus.Common/Enums/ModbusObjectType.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// List of specific types.
|
||||
/// </summary>
|
||||
public enum ModbusObjectType
|
||||
{
|
||||
/// <summary>
|
||||
/// The discrete value is a coil (read/write).
|
||||
/// </summary>
|
||||
Coil = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The discrete value is an input (read only).
|
||||
/// </summary>
|
||||
DiscreteInput = 2,
|
||||
|
||||
/// <summary>
|
||||
/// The value is a holding register (read/write).
|
||||
/// </summary>
|
||||
HoldingRegister = 3,
|
||||
|
||||
/// <summary>
|
||||
/// The value is an input register (read only).
|
||||
/// </summary>
|
||||
InputRegister = 4
|
||||
}
|
||||
}
|
||||
77
AMWD.Protocols.Modbus.Common/Exceptions/ModbusException.cs
Normal file
77
AMWD.Protocols.Modbus.Common/Exceptions/ModbusException.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents errors that occurr during Modbus requests.
|
||||
/// </summary>
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class ModbusException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModbusException"/> class.
|
||||
/// </summary>
|
||||
public ModbusException()
|
||||
: base()
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModbusException"/> class
|
||||
/// with a specified error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
public ModbusException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModbusException"/> class
|
||||
/// with a specified error message and a reference to the inner exception
|
||||
/// that is the cause of this exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
/// <param name="innerException">
|
||||
/// The exception that is the cause of the current exception,
|
||||
/// or a null reference if no inner exception is specified.
|
||||
/// </param>
|
||||
public ModbusException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
|
||||
#if !NET8_0_OR_GREATER
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModbusException"/> class
|
||||
/// with serialized data.
|
||||
/// </summary>
|
||||
/// <param name="info">
|
||||
/// The <see cref="SerializationInfo"/> that holds the serialized
|
||||
/// object data about the exception being thrown.
|
||||
/// </param>
|
||||
/// <param name="context">
|
||||
/// The <see cref="StreamingContext"/> that contains contextual
|
||||
/// information about the source or destination.
|
||||
/// </param>
|
||||
protected ModbusException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{ }
|
||||
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Modubs error code.
|
||||
/// </summary>
|
||||
#if NET6_0_OR_GREATER
|
||||
public ModbusErrorCode ErrorCode { get; init; }
|
||||
#else
|
||||
public ModbusErrorCode ErrorCode { get; set; }
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Modbus error message.
|
||||
/// </summary>
|
||||
public string ErrorMessage => ErrorCode.GetDescription();
|
||||
}
|
||||
}
|
||||
14
AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs
Normal file
14
AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal static class ArrayExtensions
|
||||
{
|
||||
public static void SwapNetworkOrder(this byte[] bytes)
|
||||
{
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
AMWD.Protocols.Modbus.Common/Extensions/EnumExtensions.cs
Normal file
30
AMWD.Protocols.Modbus.Common/Extensions/EnumExtensions.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace System
|
||||
{
|
||||
// ================================================================================================================================== //
|
||||
// Source: https://git.am-wd.de/am.wd/common/-/blob/d4b390ad911ce302cc371bb2121fa9c31db1674a/AMWD.Common/Extensions/EnumExtensions.cs //
|
||||
// ================================================================================================================================== //
|
||||
[Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal static class EnumExtensions
|
||||
{
|
||||
private static IEnumerable<TAttribute> GetAttributes<TAttribute>(this Enum value)
|
||||
where TAttribute : Attribute
|
||||
{
|
||||
var fieldInfo = value.GetType().GetField(value.ToString());
|
||||
if (fieldInfo == null)
|
||||
return Array.Empty<TAttribute>();
|
||||
|
||||
return fieldInfo.GetCustomAttributes(typeof(TAttribute), inherit: false).Cast<TAttribute>();
|
||||
}
|
||||
|
||||
private static TAttribute GetAttribute<TAttribute>(this Enum value)
|
||||
where TAttribute : Attribute
|
||||
=> value.GetAttributes<TAttribute>().FirstOrDefault();
|
||||
|
||||
public static string GetDescription(this Enum value)
|
||||
=> value.GetAttribute<DescriptionAttribute>()?.Description ?? value.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom extensions for <see cref="ModbusObject"/>s.
|
||||
/// </summary>
|
||||
public static class ModbusDecimalExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts multiple <see cref="ModbusObject"/>s into a <see cref="float"/> value.
|
||||
/// </summary>
|
||||
/// <param name="list">The list of Modbus objects.</param>
|
||||
/// <param name="startIndex">The first index to use.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The objects float value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the list is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the list is too short or the list contains mixed/incompatible objects.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">when the <paramref name="startIndex"/> is too high.</exception>
|
||||
public static float GetSingle(this IEnumerable<ModbusObject> list, int startIndex = 0, bool reverseRegisterOrder = false)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(list);
|
||||
#else
|
||||
if (list == null)
|
||||
throw new ArgumentNullException(nameof(list));
|
||||
#endif
|
||||
|
||||
int count = list.Count();
|
||||
if (count < 2)
|
||||
throw new ArgumentException("At least two registers required", nameof(list));
|
||||
|
||||
if (startIndex < 0 || startIndex + 2 > count)
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex));
|
||||
|
||||
if (!list.All(o => o.Type == ModbusObjectType.HoldingRegister) && !list.All(o => o.Type == ModbusObjectType.InputRegister))
|
||||
throw new ArgumentException("Mixed object typs found", nameof(list));
|
||||
|
||||
var registers = list.OrderBy(o => o.Address).Skip(startIndex).Take(2).ToArray();
|
||||
if (reverseRegisterOrder)
|
||||
Array.Reverse(registers);
|
||||
|
||||
byte[] blob = new byte[registers.Length * 2];
|
||||
for (int i = 0; i < registers.Length; i++)
|
||||
{
|
||||
blob[i * 2] = registers[i].HighByte;
|
||||
blob[i * 2 + 1] = registers[i].LowByte;
|
||||
}
|
||||
|
||||
blob.SwapNetworkOrder();
|
||||
return BitConverter.ToSingle(blob, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts multiple <see cref="ModbusObject"/>s into a <see cref="double"/> value.
|
||||
/// </summary>
|
||||
/// <param name="list">The list of Modbus objects.</param>
|
||||
/// <param name="startIndex">The first index to use.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The objects double value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the list is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the list is too short or the list contains mixed/incompatible objects.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">when the <paramref name="startIndex"/> is too high.</exception>
|
||||
public static double GetDouble(this IEnumerable<ModbusObject> list, int startIndex = 0, bool reverseRegisterOrder = false)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(list);
|
||||
#else
|
||||
if (list == null)
|
||||
throw new ArgumentNullException(nameof(list));
|
||||
#endif
|
||||
|
||||
int count = list.Count();
|
||||
if (count < 4)
|
||||
throw new ArgumentException("At least four registers required", nameof(list));
|
||||
|
||||
if (startIndex < 0 || startIndex + 4 > count)
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex));
|
||||
|
||||
if (!list.All(o => o.Type == ModbusObjectType.HoldingRegister) && !list.All(o => o.Type == ModbusObjectType.InputRegister))
|
||||
throw new ArgumentException("Mixed object typs found", nameof(list));
|
||||
|
||||
var registers = list.OrderBy(o => o.Address).Skip(startIndex).Take(4).ToArray();
|
||||
if (reverseRegisterOrder)
|
||||
Array.Reverse(registers);
|
||||
|
||||
byte[] blob = new byte[registers.Length * 2];
|
||||
for (int i = 0; i < registers.Length; i++)
|
||||
{
|
||||
blob[i * 2] = registers[i].HighByte;
|
||||
blob[i * 2 + 1] = registers[i].LowByte;
|
||||
}
|
||||
|
||||
blob.SwapNetworkOrder();
|
||||
return BitConverter.ToDouble(blob, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="float"/> value to a list of <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="value">The float value.</param>
|
||||
/// <param name="address">The first register address.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The list of registers.</returns>
|
||||
public static IEnumerable<HoldingRegister> ToRegister(this float value, ushort address, bool reverseRegisterOrder = false)
|
||||
{
|
||||
byte[] blob = BitConverter.GetBytes(value);
|
||||
blob.SwapNetworkOrder();
|
||||
|
||||
int numRegisters = blob.Length / 2;
|
||||
for (int i = 0; i < numRegisters; i++)
|
||||
{
|
||||
int addr = reverseRegisterOrder
|
||||
? address + numRegisters - 1 - i
|
||||
: address + i;
|
||||
|
||||
yield return new HoldingRegister
|
||||
{
|
||||
Address = (ushort)addr,
|
||||
HighByte = blob[i * 2],
|
||||
LowByte = blob[i * 2 + 1]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="double"/> value to a list of <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="value">The double value.</param>
|
||||
/// <param name="address">The first register address.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The list of registers.</returns>
|
||||
public static IEnumerable<HoldingRegister> ToRegister(this double value, ushort address, bool reverseRegisterOrder = false)
|
||||
{
|
||||
byte[] blob = BitConverter.GetBytes(value);
|
||||
blob.SwapNetworkOrder();
|
||||
|
||||
int numRegisters = blob.Length / 2;
|
||||
for (int i = 0; i < numRegisters; i++)
|
||||
{
|
||||
int addr = reverseRegisterOrder
|
||||
? address + numRegisters - 1 - i
|
||||
: address + i;
|
||||
|
||||
yield return new HoldingRegister
|
||||
{
|
||||
Address = (ushort)addr,
|
||||
HighByte = blob[i * 2],
|
||||
LowByte = blob[i * 2 + 1]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
175
AMWD.Protocols.Modbus.Common/Extensions/ModbusExtensions.cs
Normal file
175
AMWD.Protocols.Modbus.Common/Extensions/ModbusExtensions.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom extensions for <see cref="ModbusObject"/>s.
|
||||
/// </summary>
|
||||
public static class ModbusExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a <see cref="ModbusObject"/> into a <see cref="bool"/> value.
|
||||
/// </summary>
|
||||
/// <param name="obj">The Modbus object.</param>
|
||||
/// <returns>The objects bool value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the object is null.</exception>
|
||||
public static bool GetBoolean(this ModbusObject obj)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(obj);
|
||||
#else
|
||||
if (obj == null)
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
#endif
|
||||
|
||||
if (obj is Coil coil)
|
||||
return coil.Value;
|
||||
|
||||
if (obj is DiscreteInput discreteInput)
|
||||
return discreteInput.Value;
|
||||
|
||||
return obj.HighByte > 0 || obj.LowByte > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts multiple <see cref="ModbusObject"/>s into a <see cref="string"/> value.
|
||||
/// </summary>
|
||||
/// <param name="list">The list of Modbus objects.</param>
|
||||
/// <param name="length">The number of registers to use.</param>
|
||||
/// <param name="startIndex">The first index to use.</param>
|
||||
/// <param name="encoding">The encoding used to convert the text. (Default: <see cref="Encoding.ASCII"/>)</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <param name="reverseByteOrderPerRegister">Indicates whether to reverse high and low byte per register.</param>
|
||||
/// <returns>The objects text value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the list is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the list is too short or the list contains mixed/incompatible objects.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">when the <paramref name="startIndex"/> is too high.</exception>
|
||||
public static string GetString(this IEnumerable<ModbusObject> list, int length, int startIndex = 0, Encoding encoding = null, bool reverseRegisterOrder = false, bool reverseByteOrderPerRegister = false)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(list);
|
||||
#else
|
||||
if (list == null)
|
||||
throw new ArgumentNullException(nameof(list));
|
||||
#endif
|
||||
|
||||
int count = list.Count();
|
||||
if (count < length)
|
||||
throw new ArgumentException($"At least {length} registers required", nameof(list));
|
||||
|
||||
if (startIndex < 0 || startIndex + length > count)
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex));
|
||||
|
||||
if (!list.All(o => o.Type == ModbusObjectType.HoldingRegister) && !list.All(o => o.Type == ModbusObjectType.InputRegister))
|
||||
throw new ArgumentException("Mixed object types found", nameof(list));
|
||||
|
||||
var registers = list.OrderBy(o => o.Address).Skip(startIndex).Take(length).ToArray();
|
||||
if (reverseRegisterOrder)
|
||||
Array.Reverse(registers);
|
||||
|
||||
byte[] blob = new byte[registers.Length * 2];
|
||||
for (int i = 0; i < registers.Length; i++)
|
||||
{
|
||||
blob[i * 2] = reverseByteOrderPerRegister
|
||||
? registers[i].LowByte
|
||||
: registers[i].HighByte;
|
||||
|
||||
blob[i * 2 + 1] = reverseByteOrderPerRegister
|
||||
? registers[i].HighByte
|
||||
: registers[i].LowByte;
|
||||
}
|
||||
|
||||
string text = (encoding ?? Encoding.ASCII).GetString(blob).Trim([' ', '\t', '\0', '\r', '\n']);
|
||||
int nullIndex = text.IndexOf('\0');
|
||||
|
||||
if (nullIndex > 0)
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
return text[..nullIndex];
|
||||
#else
|
||||
return text.Substring(0, nullIndex);
|
||||
#endif
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="bool"/> value to a <see cref="Coil"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The bool value.</param>
|
||||
/// <param name="address">The coil address.</param>
|
||||
/// <returns>The coil.</returns>
|
||||
public static Coil ToCoil(this bool value, ushort address)
|
||||
{
|
||||
return new Coil
|
||||
{
|
||||
Address = address,
|
||||
Value = value
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="bool"/> value to a <see cref="HoldingRegister"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The bool value.</param>
|
||||
/// <param name="address">The register address.</param>
|
||||
/// <returns>The register.</returns>
|
||||
public static HoldingRegister ToRegister(this bool value, ushort address)
|
||||
{
|
||||
return new HoldingRegister
|
||||
{
|
||||
Address = address,
|
||||
Value = (ushort)(value ? 1 : 0)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="string"/> value to a <see cref="HoldingRegister"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The text.</param>
|
||||
/// <param name="address">The address of the text.</param>
|
||||
/// <param name="encoding">The encoding used to convert the text. (Default: <see cref="Encoding.ASCII"/>)</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <param name="reverseByteOrderPerRegister">Indicates whether to reverse high and low byte per register.</param>
|
||||
/// <returns>The registers.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the text is null.</exception>
|
||||
public static IEnumerable<HoldingRegister> ToRegisters(this string value, ushort address, Encoding encoding = null, bool reverseRegisterOrder = false, bool reverseByteOrderPerRegister = false)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
#else
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
#endif
|
||||
|
||||
byte[] blob = (encoding ?? Encoding.ASCII).GetBytes(value);
|
||||
int numRegisters = (int)Math.Ceiling(blob.Length / 2.0);
|
||||
|
||||
for (int i = 0; i < numRegisters; i++)
|
||||
{
|
||||
int addr = reverseRegisterOrder
|
||||
? address + numRegisters - 1 - i
|
||||
: address + i;
|
||||
|
||||
var register = new HoldingRegister
|
||||
{
|
||||
Address = (ushort)addr,
|
||||
|
||||
HighByte = reverseByteOrderPerRegister
|
||||
? (i * 2 + 1 < blob.Length ? blob[i * 2 + 1] : (byte)0)
|
||||
: blob[i * 2],
|
||||
|
||||
LowByte = reverseByteOrderPerRegister
|
||||
? blob[i * 2]
|
||||
: (i * 2 + 1 < blob.Length ? blob[i * 2 + 1] : (byte)0)
|
||||
};
|
||||
|
||||
yield return register;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom extensions for <see cref="ModbusObject"/>s.
|
||||
/// </summary>
|
||||
public static class ModbusSignedExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a <see cref="ModbusObject"/> into a <see cref="sbyte"/> value.
|
||||
/// </summary>
|
||||
/// <param name="obj">The Modbus object.</param>
|
||||
/// <returns>The objects signed byte value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the object is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the wrong types are provided.</exception>
|
||||
public static sbyte GetSByte(this ModbusObject obj)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(obj);
|
||||
#else
|
||||
if (obj == null)
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
#endif
|
||||
|
||||
if (obj is HoldingRegister holdingRegister)
|
||||
return (sbyte)holdingRegister.Value;
|
||||
|
||||
if (obj is InputRegister inputRegister)
|
||||
return (sbyte)inputRegister.Value;
|
||||
|
||||
throw new ArgumentException($"The object type '{obj.GetType()}' is invalid", nameof(obj));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="ModbusObject"/> into a <see cref="short"/> value.
|
||||
/// </summary>
|
||||
/// <param name="obj">The Modbus object.</param>
|
||||
/// <returns>The objects short value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the object is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the wrong types are provided.</exception>
|
||||
public static short GetInt16(this ModbusObject obj)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(obj);
|
||||
#else
|
||||
if (obj == null)
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
#endif
|
||||
|
||||
if (obj is HoldingRegister holdingRegister)
|
||||
return (short)holdingRegister.Value;
|
||||
|
||||
if (obj is InputRegister inputRegister)
|
||||
return (short)inputRegister.Value;
|
||||
|
||||
throw new ArgumentException($"The object type '{obj.GetType()}' is invalid", nameof(obj));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts multiple <see cref="ModbusObject"/>s into a <see cref="int"/> value.
|
||||
/// </summary>
|
||||
/// <param name="list">The list of Modbus objects.</param>
|
||||
/// <param name="startIndex">The first index to use.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The objects int value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the list is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the list is too short or the list contains mixed/incompatible objects.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">when the <paramref name="startIndex"/> is too high.</exception>
|
||||
public static int GetInt32(this IEnumerable<ModbusObject> list, int startIndex = 0, bool reverseRegisterOrder = false)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(list);
|
||||
#else
|
||||
if (list == null)
|
||||
throw new ArgumentNullException(nameof(list));
|
||||
#endif
|
||||
|
||||
int count = list.Count();
|
||||
if (count < 2)
|
||||
throw new ArgumentException("At least two registers required", nameof(list));
|
||||
|
||||
if (startIndex < 0 || startIndex + 2 > count)
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex));
|
||||
|
||||
if (!list.All(o => o.Type == ModbusObjectType.HoldingRegister) && !list.All(o => o.Type == ModbusObjectType.InputRegister))
|
||||
throw new ArgumentException("Mixed object typs found", nameof(list));
|
||||
|
||||
var registers = list.OrderBy(o => o.Address).Skip(startIndex).Take(2).ToArray();
|
||||
if (reverseRegisterOrder)
|
||||
Array.Reverse(registers);
|
||||
|
||||
byte[] blob = new byte[registers.Length * 2];
|
||||
for (int i = 0; i < registers.Length; i++)
|
||||
{
|
||||
blob[i * 2] = registers[i].HighByte;
|
||||
blob[i * 2 + 1] = registers[i].LowByte;
|
||||
}
|
||||
|
||||
blob.SwapNetworkOrder();
|
||||
return BitConverter.ToInt32(blob, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts multiple <see cref="ModbusObject"/>s into a <see cref="long"/> value.
|
||||
/// </summary>
|
||||
/// <param name="list">The list of Modbus objects.</param>
|
||||
/// <param name="startIndex">The first index to use.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The objects long value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the list is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the list is too short or the list contains mixed/incompatible objects.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">when the <paramref name="startIndex"/> is too high.</exception>
|
||||
public static long GetInt64(this IEnumerable<ModbusObject> list, int startIndex = 0, bool reverseRegisterOrder = false)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(list);
|
||||
#else
|
||||
if (list == null)
|
||||
throw new ArgumentNullException(nameof(list));
|
||||
#endif
|
||||
|
||||
int count = list.Count();
|
||||
if (count < 4)
|
||||
throw new ArgumentException("At least four registers required", nameof(list));
|
||||
|
||||
if (startIndex < 0 || startIndex + 4 > count)
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex));
|
||||
|
||||
if (!list.All(o => o.Type == ModbusObjectType.HoldingRegister) && !list.All(o => o.Type == ModbusObjectType.InputRegister))
|
||||
throw new ArgumentException("Mixed object typs found", nameof(list));
|
||||
|
||||
var registers = list.OrderBy(o => o.Address).Skip(startIndex).Take(4).ToArray();
|
||||
if (reverseRegisterOrder)
|
||||
Array.Reverse(registers);
|
||||
|
||||
byte[] blob = new byte[registers.Length * 2];
|
||||
for (int i = 0; i < registers.Length; i++)
|
||||
{
|
||||
blob[i * 2] = registers[i].HighByte;
|
||||
blob[i * 2 + 1] = registers[i].LowByte;
|
||||
}
|
||||
|
||||
blob.SwapNetworkOrder();
|
||||
return BitConverter.ToInt64(blob, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="sbyte"/> value to a <see cref="HoldingRegister"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The signed byte value.</param>
|
||||
/// <param name="address">The register address.</param>
|
||||
/// <returns>The register.</returns>
|
||||
public static HoldingRegister ToRegister(this sbyte value, ushort address)
|
||||
{
|
||||
return new HoldingRegister
|
||||
{
|
||||
Address = address,
|
||||
LowByte = (byte)value
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="short"/> value to a <see cref="HoldingRegister"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The short value.</param>
|
||||
/// <param name="address">The register address.</param>
|
||||
/// <returns>The register.</returns>
|
||||
public static HoldingRegister ToRegister(this short value, ushort address)
|
||||
{
|
||||
byte[] blob = BitConverter.GetBytes(value);
|
||||
blob.SwapNetworkOrder();
|
||||
|
||||
return new HoldingRegister
|
||||
{
|
||||
Address = address,
|
||||
HighByte = blob[0],
|
||||
LowByte = blob[1]
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="int"/> value to a list of <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="value">The int value.</param>
|
||||
/// <param name="address">The first register address.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The list of registers.</returns>
|
||||
public static IEnumerable<HoldingRegister> ToRegister(this int value, ushort address, bool reverseRegisterOrder = false)
|
||||
{
|
||||
byte[] blob = BitConverter.GetBytes(value);
|
||||
blob.SwapNetworkOrder();
|
||||
|
||||
int numRegisters = blob.Length / 2;
|
||||
for (int i = 0; i < numRegisters; i++)
|
||||
{
|
||||
int addr = reverseRegisterOrder
|
||||
? address + numRegisters - 1 - i
|
||||
: address + i;
|
||||
|
||||
yield return new HoldingRegister
|
||||
{
|
||||
Address = (ushort)addr,
|
||||
HighByte = blob[i * 2],
|
||||
LowByte = blob[i * 2 + 1]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="long"/> value to a list of <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="value">The long value.</param>
|
||||
/// <param name="address">The first register address.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The list of registers.</returns>
|
||||
public static IEnumerable<HoldingRegister> ToRegister(this long value, ushort address, bool reverseRegisterOrder = false)
|
||||
{
|
||||
byte[] blob = BitConverter.GetBytes(value);
|
||||
blob.SwapNetworkOrder();
|
||||
|
||||
int numRegisters = blob.Length / 2;
|
||||
for (int i = 0; i < numRegisters; i++)
|
||||
{
|
||||
int addr = reverseRegisterOrder
|
||||
? address + numRegisters - 1 - i
|
||||
: address + i;
|
||||
|
||||
yield return new HoldingRegister
|
||||
{
|
||||
Address = (ushort)addr,
|
||||
HighByte = blob[i * 2],
|
||||
LowByte = blob[i * 2 + 1]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom extensions for <see cref="ModbusObject"/>s.
|
||||
/// </summary>
|
||||
public static class ModbusUnsignedExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a <see cref="ModbusObject"/> into a <see cref="byte"/> value.
|
||||
/// </summary>
|
||||
/// <param name="obj">The Modbus object.</param>
|
||||
/// <returns>The objects byte value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the object is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the wrong types are provided.</exception>
|
||||
public static byte GetByte(this ModbusObject obj)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(obj);
|
||||
#else
|
||||
if (obj == null)
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
#endif
|
||||
|
||||
if (obj is HoldingRegister holdingRegister)
|
||||
return (byte)holdingRegister.Value;
|
||||
|
||||
if (obj is InputRegister inputRegister)
|
||||
return (byte)inputRegister.Value;
|
||||
|
||||
throw new ArgumentException($"The object type '{obj.GetType()}' is invalid", nameof(obj));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="ModbusObject"/> into a <see cref="ushort"/> value.
|
||||
/// </summary>
|
||||
/// <param name="obj">The Modbus object.</param>
|
||||
/// <returns>The objects unsigned short value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the object is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the wrong types are provided.</exception>
|
||||
public static ushort GetUInt16(this ModbusObject obj)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(obj);
|
||||
#else
|
||||
if (obj == null)
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
#endif
|
||||
|
||||
if (obj is HoldingRegister holdingRegister)
|
||||
return holdingRegister.Value;
|
||||
|
||||
if (obj is InputRegister inputRegister)
|
||||
return inputRegister.Value;
|
||||
|
||||
throw new ArgumentException($"The object type '{obj.GetType()}' is invalid", nameof(obj));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts multiple <see cref="ModbusObject"/>s into a <see cref="uint"/> value.
|
||||
/// </summary>
|
||||
/// <param name="list">The list of Modbus objects.</param>
|
||||
/// <param name="startIndex">The first index to use.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The objects unsigned int value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the list is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the list is too short or the list contains mixed/incompatible objects.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">when the <paramref name="startIndex"/> is too high.</exception>
|
||||
public static uint GetUInt32(this IEnumerable<ModbusObject> list, int startIndex = 0, bool reverseRegisterOrder = false)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(list);
|
||||
#else
|
||||
if (list == null)
|
||||
throw new ArgumentNullException(nameof(list));
|
||||
#endif
|
||||
|
||||
int count = list.Count();
|
||||
if (count < 2)
|
||||
throw new ArgumentException("At least two registers required", nameof(list));
|
||||
|
||||
if (startIndex < 0 || startIndex + 2 > count)
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex));
|
||||
|
||||
if (!list.All(o => o.Type == ModbusObjectType.HoldingRegister) && !list.All(o => o.Type == ModbusObjectType.InputRegister))
|
||||
throw new ArgumentException("Mixed object typs found", nameof(list));
|
||||
|
||||
var registers = list.OrderBy(o => o.Address).Skip(startIndex).Take(2).ToArray();
|
||||
if (reverseRegisterOrder)
|
||||
Array.Reverse(registers);
|
||||
|
||||
byte[] blob = new byte[registers.Length * 2];
|
||||
for (int i = 0; i < registers.Length; i++)
|
||||
{
|
||||
blob[i * 2] = registers[i].HighByte;
|
||||
blob[i * 2 + 1] = registers[i].LowByte;
|
||||
}
|
||||
|
||||
blob.SwapNetworkOrder();
|
||||
return BitConverter.ToUInt32(blob, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts multiple <see cref="ModbusObject"/>s into a <see cref="ulong"/> value.
|
||||
/// </summary>
|
||||
/// <param name="list">The list of Modbus objects.</param>
|
||||
/// <param name="startIndex">The first index to use.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The objects unsigned long value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the list is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the list is too short or the list contains mixed/incompatible objects.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">when the <paramref name="startIndex"/> is too high.</exception>
|
||||
public static ulong GetUInt64(this IEnumerable<ModbusObject> list, int startIndex = 0, bool reverseRegisterOrder = false)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(list);
|
||||
#else
|
||||
if (list == null)
|
||||
throw new ArgumentNullException(nameof(list));
|
||||
#endif
|
||||
|
||||
int count = list.Count();
|
||||
if (count < 4)
|
||||
throw new ArgumentException("At least four registers required", nameof(list));
|
||||
|
||||
if (startIndex < 0 || startIndex + 4 > count)
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex));
|
||||
|
||||
if (!list.All(o => o.Type == ModbusObjectType.HoldingRegister) && !list.All(o => o.Type == ModbusObjectType.InputRegister))
|
||||
throw new ArgumentException("Mixed object typs found", nameof(list));
|
||||
|
||||
var registers = list.OrderBy(o => o.Address).Skip(startIndex).Take(4).ToArray();
|
||||
if (reverseRegisterOrder)
|
||||
Array.Reverse(registers);
|
||||
|
||||
byte[] blob = new byte[registers.Length * 2];
|
||||
for (int i = 0; i < registers.Length; i++)
|
||||
{
|
||||
blob[i * 2] = registers[i].HighByte;
|
||||
blob[i * 2 + 1] = registers[i].LowByte;
|
||||
}
|
||||
|
||||
blob.SwapNetworkOrder();
|
||||
return BitConverter.ToUInt64(blob, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="byte"/> value to a <see cref="HoldingRegister"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The byte value.</param>
|
||||
/// <param name="address">The register address.</param>
|
||||
/// <returns>The register.</returns>
|
||||
public static HoldingRegister ToRegister(this byte value, ushort address)
|
||||
{
|
||||
return new HoldingRegister
|
||||
{
|
||||
Address = address,
|
||||
LowByte = value
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="ushort"/> value to a <see cref="HoldingRegister"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The unsigned short value.</param>
|
||||
/// <param name="address">The register address.</param>
|
||||
/// <returns>The register.</returns>
|
||||
public static HoldingRegister ToRegister(this ushort value, ushort address)
|
||||
{
|
||||
byte[] blob = BitConverter.GetBytes(value);
|
||||
blob.SwapNetworkOrder();
|
||||
|
||||
return new HoldingRegister
|
||||
{
|
||||
Address = address,
|
||||
HighByte = blob[0],
|
||||
LowByte = blob[1]
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="uint"/> value to a list of <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="value">The unsigned int value.</param>
|
||||
/// <param name="address">The first register address.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The list of registers.</returns>
|
||||
public static IEnumerable<HoldingRegister> ToRegister(this uint value, ushort address, bool reverseRegisterOrder = false)
|
||||
{
|
||||
byte[] blob = BitConverter.GetBytes(value);
|
||||
blob.SwapNetworkOrder();
|
||||
|
||||
int numRegisters = blob.Length / 2;
|
||||
for (int i = 0; i < numRegisters; i++)
|
||||
{
|
||||
int addr = reverseRegisterOrder
|
||||
? address + numRegisters - 1 - i
|
||||
: address + i;
|
||||
|
||||
yield return new HoldingRegister
|
||||
{
|
||||
Address = (ushort)addr,
|
||||
HighByte = blob[i * 2],
|
||||
LowByte = blob[i * 2 + 1]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="ulong"/> value to a list of <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="value">The unsigned long value.</param>
|
||||
/// <param name="address">The first register address.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The list of registers.</returns>
|
||||
public static IEnumerable<HoldingRegister> ToRegister(this ulong value, ushort address, bool reverseRegisterOrder = false)
|
||||
{
|
||||
byte[] blob = BitConverter.GetBytes(value);
|
||||
blob.SwapNetworkOrder();
|
||||
|
||||
int numRegisters = blob.Length / 2;
|
||||
for (int i = 0; i < numRegisters; i++)
|
||||
{
|
||||
int addr = reverseRegisterOrder
|
||||
? address + numRegisters - 1 - i
|
||||
: address + i;
|
||||
|
||||
yield return new HoldingRegister
|
||||
{
|
||||
Address = (ushort)addr,
|
||||
HighByte = blob[i * 2],
|
||||
LowByte = blob[i * 2 + 1]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
AMWD.Protocols.Modbus.Common/InternalsVisibleTo.cs
Normal file
3
AMWD.Protocols.Modbus.Common/InternalsVisibleTo.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("AMWD.Protocols.Modbus.Tests")]
|
||||
28
AMWD.Protocols.Modbus.Common/Models/Coil.cs
Normal file
28
AMWD.Protocols.Modbus.Common/Models/Coil.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a coil.
|
||||
/// </summary>
|
||||
public class Coil : ModbusObject
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ModbusObjectType Type => ModbusObjectType.Coil;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the coil is on or off.
|
||||
/// </summary>
|
||||
public bool Value
|
||||
{
|
||||
get => HighByte == 0xFF;
|
||||
set
|
||||
{
|
||||
HighByte = (byte)(value ? 0xFF : 0x00);
|
||||
LowByte = 0x00;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
=> $"Coil #{Address} | {(Value ? "ON" : "OFF")}";
|
||||
}
|
||||
}
|
||||
20
AMWD.Protocols.Modbus.Common/Models/DiscreteInput.cs
Normal file
20
AMWD.Protocols.Modbus.Common/Models/DiscreteInput.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a discrete input.
|
||||
/// </summary>
|
||||
public class DiscreteInput : ModbusObject
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ModbusObjectType Type => ModbusObjectType.DiscreteInput;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the discrete input is on or off.
|
||||
/// </summary>
|
||||
public bool Value => HighByte == 0xFF;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
=> $"Discrete Input #{Address} | {(Value ? "ON" : "OFF")}";
|
||||
}
|
||||
}
|
||||
41
AMWD.Protocols.Modbus.Common/Models/HoldingRegister.cs
Normal file
41
AMWD.Protocols.Modbus.Common/Models/HoldingRegister.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a holding register.
|
||||
/// </summary>
|
||||
public class HoldingRegister : ModbusObject
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ModbusObjectType Type => ModbusObjectType.HoldingRegister;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the holding register.
|
||||
/// </summary>
|
||||
public ushort Value
|
||||
{
|
||||
get
|
||||
{
|
||||
byte[] blob = [HighByte, LowByte];
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(blob);
|
||||
|
||||
return BitConverter.ToUInt16(blob, 0);
|
||||
}
|
||||
set
|
||||
{
|
||||
byte[] blob = BitConverter.GetBytes(value);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(blob);
|
||||
|
||||
HighByte = blob[0];
|
||||
LowByte = blob[1];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
=> $"Holding Register #{Address} | {Value} | HI: {HighByte:X2}, LO: {LowByte:X2}";
|
||||
}
|
||||
}
|
||||
32
AMWD.Protocols.Modbus.Common/Models/InputRegister.cs
Normal file
32
AMWD.Protocols.Modbus.Common/Models/InputRegister.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a input register.
|
||||
/// </summary>
|
||||
public class InputRegister : ModbusObject
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ModbusObjectType Type => ModbusObjectType.InputRegister;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the input register.
|
||||
/// </summary>
|
||||
public ushort Value
|
||||
{
|
||||
get
|
||||
{
|
||||
byte[] blob = [HighByte, LowByte];
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(blob);
|
||||
|
||||
return BitConverter.ToUInt16(blob, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
=> $"Input Register #{Address} | {Value} | HI: {HighByte:X2}, LO: {LowByte:X2}";
|
||||
}
|
||||
}
|
||||
56
AMWD.Protocols.Modbus.Common/Models/ModbusObject.cs
Normal file
56
AMWD.Protocols.Modbus.Common/Models/ModbusObject.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the base of all Modbus specific objects.
|
||||
/// </summary>
|
||||
public abstract class ModbusObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the type of the object.
|
||||
/// </summary>
|
||||
public abstract ModbusObjectType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the address of the object.
|
||||
/// </summary>
|
||||
public ushort Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the high byte of the value.
|
||||
/// </summary>
|
||||
public byte HighByte { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the low byte of the value.
|
||||
/// </summary>
|
||||
public byte LowByte { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is not ModbusObject mo)
|
||||
return false;
|
||||
|
||||
return Type == mo.Type
|
||||
&& Address == mo.Address
|
||||
&& HighByte == mo.HighByte
|
||||
&& LowByte == mo.LowByte;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public override int GetHashCode()
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
return HashCode.Combine(Type, Address, HighByte, LowByte);
|
||||
#else
|
||||
return Type.GetHashCode()
|
||||
^ Address.GetHashCode()
|
||||
^ HighByte.GetHashCode()
|
||||
^ LowByte.GetHashCode();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
646
AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs
Normal file
646
AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs
Normal file
@@ -0,0 +1,646 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
{
|
||||
/// <summary>
|
||||
/// Default implementation of the Modbus TCP protocol.
|
||||
/// </summary>
|
||||
public class TcpProtocol : IModbusProtocol
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly object _lock = new();
|
||||
private ushort _transactionId = 0x0000;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Constants
|
||||
|
||||
/// <summary>
|
||||
/// The minimum allowed unit id specified by the Modbus TCP protocol.
|
||||
/// </summary>
|
||||
public const byte MIN_UNIT_ID = 0x00;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum allowed unit id specified by the Modbus TCP protocol.
|
||||
/// </summary>
|
||||
public const byte MAX_UNIT_ID = 0xFF;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum allowed read count specified by the Modbus TCP protocol.
|
||||
/// </summary>
|
||||
public const ushort MIN_READ_COUNT = 0x01;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum allowed write count specified by the Modbus TCP protocol.
|
||||
/// </summary>
|
||||
public const ushort MIN_WRITE_COUNT = 0x01;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum allowed read count for discrete values specified by the Modbus TCP protocol.
|
||||
/// </summary>
|
||||
public const ushort MAX_DISCRETE_READ_COUNT = 0x07D0; // 2000
|
||||
|
||||
/// <summary>
|
||||
/// The maximum allowed write count for discrete values specified by the Modbus TCP protocol.
|
||||
/// </summary>
|
||||
public const ushort MAX_DISCRETE_WRITE_COUNT = 0x07B0; // 1968
|
||||
|
||||
/// <summary>
|
||||
/// The maximum allowed read count for registers specified by the Modbus TCP protocol.
|
||||
/// </summary>
|
||||
public const ushort MAX_REGISTER_READ_COUNT = 0x007D; // 125
|
||||
|
||||
/// <summary>
|
||||
/// The maximum allowed write count for registers specified by the Modbus TCP protocol.
|
||||
/// </summary>
|
||||
public const ushort MAX_REGISTER_WRITE_COUNT = 0x007B; // 123
|
||||
|
||||
#endregion Constants
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "TCP";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to disable the transaction id usage.
|
||||
/// </summary>
|
||||
public bool DisableTransactionId { get; set; }
|
||||
|
||||
#region Read
|
||||
|
||||
/// <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));
|
||||
|
||||
if (ushort.MaxValue < (startAddress + count - 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(count), $"Combination of {nameof(startAddress)} and {nameof(count)} exceeds the addressation limit of {ushort.MaxValue}");
|
||||
|
||||
byte[] request = new byte[12];
|
||||
|
||||
byte[] header = GetHeader(unitId, 6);
|
||||
Array.Copy(header, 0, request, 0, header.Length);
|
||||
|
||||
// Function code
|
||||
request[7] = (byte)ModbusFunctionCode.ReadCoils;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = ToNetworkBytes(startAddress);
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = ToNetworkBytes(count);
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<Coil> DeserializeReadCoils(IReadOnlyList<byte> response)
|
||||
{
|
||||
int baseOffset = 9;
|
||||
if (response[8] != response.Count - baseOffset)
|
||||
throw new ModbusException("Coil byte count does not match.");
|
||||
|
||||
int count = response[8] * 8;
|
||||
var coils = new List<Coil>();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int bytePosition = i / 8;
|
||||
int bitPosition = i % 8;
|
||||
|
||||
int value = response[baseOffset + bytePosition] & (1 << bitPosition);
|
||||
coils.Add(new Coil
|
||||
{
|
||||
Address = (ushort)i,
|
||||
Value = value > 0
|
||||
});
|
||||
}
|
||||
|
||||
return coils;
|
||||
}
|
||||
|
||||
/// <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));
|
||||
|
||||
if (ushort.MaxValue < (startAddress + count - 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(count), $"Combination of {nameof(startAddress)} and {nameof(count)} exceeds the addressation limit of {ushort.MaxValue}");
|
||||
|
||||
byte[] request = new byte[12];
|
||||
|
||||
byte[] header = GetHeader(unitId, 6);
|
||||
Array.Copy(header, 0, request, 0, header.Length);
|
||||
|
||||
// Function code
|
||||
request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = ToNetworkBytes(startAddress);
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = ToNetworkBytes(count);
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<DiscreteInput> DeserializeReadDiscreteInputs(IReadOnlyList<byte> response)
|
||||
{
|
||||
int baseOffset = 9;
|
||||
if (response[8] != response.Count - baseOffset)
|
||||
throw new ModbusException("Discrete input byte count does not match.");
|
||||
|
||||
int count = response[8] * 8;
|
||||
var discreteInputs = new List<DiscreteInput>();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int bytePosition = i / 8;
|
||||
int bitPosition = i % 8;
|
||||
|
||||
int value = response[baseOffset + bytePosition] & (1 << bitPosition);
|
||||
discreteInputs.Add(new DiscreteInput
|
||||
{
|
||||
Address = (ushort)i,
|
||||
HighByte = (byte)(value > 0 ? 0xFF : 0x00)
|
||||
});
|
||||
}
|
||||
|
||||
return discreteInputs;
|
||||
}
|
||||
|
||||
/// <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));
|
||||
|
||||
if (ushort.MaxValue < (startAddress + count - 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(count), $"Combination of {nameof(startAddress)} and {nameof(count)} exceeds the addressation limit of {ushort.MaxValue}");
|
||||
|
||||
byte[] request = new byte[12];
|
||||
|
||||
byte[] header = GetHeader(unitId, 6);
|
||||
Array.Copy(header, 0, request, 0, header.Length);
|
||||
|
||||
// Function code
|
||||
request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = ToNetworkBytes(startAddress);
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = ToNetworkBytes(count);
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<HoldingRegister> DeserializeReadHoldingRegisters(IReadOnlyList<byte> response)
|
||||
{
|
||||
int baseOffset = 9;
|
||||
if (response[8] != response.Count - baseOffset)
|
||||
throw new ModbusException("Holding register byte count does not match.");
|
||||
|
||||
int count = response[8] / 2;
|
||||
var holdingRegisters = new List<HoldingRegister>();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
holdingRegisters.Add(new HoldingRegister
|
||||
{
|
||||
Address = (ushort)i,
|
||||
HighByte = response[baseOffset + i * 2],
|
||||
LowByte = response[baseOffset + i * 2 + 1]
|
||||
});
|
||||
}
|
||||
|
||||
return holdingRegisters;
|
||||
}
|
||||
|
||||
/// <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));
|
||||
|
||||
if (ushort.MaxValue < (startAddress + count - 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(count), $"Combination of {nameof(startAddress)} and {nameof(count)} exceeds the addressation limit of {ushort.MaxValue}");
|
||||
|
||||
byte[] request = new byte[12];
|
||||
|
||||
byte[] header = GetHeader(unitId, 6);
|
||||
Array.Copy(header, 0, request, 0, header.Length);
|
||||
|
||||
// Function code
|
||||
request[7] = (byte)ModbusFunctionCode.ReadInputRegisters;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = ToNetworkBytes(startAddress);
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = ToNetworkBytes(count);
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<InputRegister> DeserializeReadInputRegisters(IReadOnlyList<byte> response)
|
||||
{
|
||||
int baseOffset = 9;
|
||||
if (response[8] != response.Count - baseOffset)
|
||||
throw new ModbusException("Input register byte count does not match.");
|
||||
|
||||
int count = response[8] / 2;
|
||||
var inputRegisters = new List<InputRegister>();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
inputRegisters.Add(new InputRegister
|
||||
{
|
||||
Address = (ushort)i,
|
||||
HighByte = response[baseOffset + i * 2],
|
||||
LowByte = response[baseOffset + i * 2 + 1]
|
||||
});
|
||||
}
|
||||
|
||||
return inputRegisters;
|
||||
}
|
||||
|
||||
#endregion Read
|
||||
|
||||
#region Write
|
||||
|
||||
/// <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
|
||||
if (coil == null)
|
||||
throw new ArgumentNullException(nameof(coil));
|
||||
#endif
|
||||
|
||||
byte[] request = new byte[12];
|
||||
|
||||
byte[] header = GetHeader(unitId, 6);
|
||||
Array.Copy(header, 0, request, 0, header.Length);
|
||||
|
||||
// Function code
|
||||
request[7] = (byte)ModbusFunctionCode.WriteSingleCoil;
|
||||
|
||||
byte[] addrBytes = ToNetworkBytes(coil.Address);
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
request[10] = coil.HighByte;
|
||||
request[11] = coil.LowByte;
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Coil DeserializeWriteSingleCoil(IReadOnlyList<byte> response)
|
||||
{
|
||||
return new Coil
|
||||
{
|
||||
Address = ToNetworkUInt16(response.Skip(8).Take(2).ToArray()),
|
||||
HighByte = response[10],
|
||||
LowByte = response[11]
|
||||
};
|
||||
}
|
||||
|
||||
/// <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
|
||||
if (register == null)
|
||||
throw new ArgumentNullException(nameof(register));
|
||||
#endif
|
||||
|
||||
byte[] request = new byte[12];
|
||||
|
||||
byte[] header = GetHeader(unitId, 6);
|
||||
Array.Copy(header, 0, request, 0, header.Length);
|
||||
|
||||
// Function code
|
||||
request[7] = (byte)ModbusFunctionCode.WriteSingleRegister;
|
||||
|
||||
byte[] addrBytes = ToNetworkBytes(register.Address);
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
request[10] = register.HighByte;
|
||||
request[11] = register.LowByte;
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public HoldingRegister DeserializeWriteSingleHoldingRegister(IReadOnlyList<byte> response)
|
||||
{
|
||||
return new HoldingRegister
|
||||
{
|
||||
Address = ToNetworkUInt16(response.Skip(8).Take(2).ToArray()),
|
||||
HighByte = response[10],
|
||||
LowByte = response[11]
|
||||
};
|
||||
}
|
||||
|
||||
/// <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
|
||||
if (coils == null)
|
||||
throw new ArgumentNullException(nameof(coils));
|
||||
#endif
|
||||
|
||||
var orderedList = coils.OrderBy(c => c.Address).ToList();
|
||||
if (orderedList.Count < MIN_WRITE_COUNT || MAX_DISCRETE_WRITE_COUNT < orderedList.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(coils), $"At least {MIN_WRITE_COUNT} or max. {MAX_DISCRETE_WRITE_COUNT} coils can be written at once.");
|
||||
|
||||
int addrCount = coils.Select(c => c.Address).Distinct().Count();
|
||||
if (orderedList.Count != addrCount)
|
||||
throw new ArgumentException("One or more duplicate coils found.", nameof(coils));
|
||||
|
||||
ushort firstAddress = orderedList.First().Address;
|
||||
ushort lastAddress = orderedList.Last().Address;
|
||||
|
||||
if (firstAddress + orderedList.Count - 1 != lastAddress)
|
||||
throw new ArgumentException("Gap in coil list found.", nameof(coils));
|
||||
|
||||
byte byteCount = (byte)Math.Ceiling(orderedList.Count / 8.0);
|
||||
byte[] request = new byte[13 + byteCount];
|
||||
|
||||
byte[] header = GetHeader(unitId, byteCount + 7);
|
||||
Array.Copy(header, 0, request, 0, header.Length);
|
||||
|
||||
request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils;
|
||||
|
||||
byte[] addrBytes = ToNetworkBytes(firstAddress);
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
byte[] countBytes = ToNetworkBytes((ushort)orderedList.Count);
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
request[12] = byteCount;
|
||||
|
||||
int baseOffset = 13;
|
||||
for (int i = 0; i < orderedList.Count; i++)
|
||||
{
|
||||
int bytePosition = i / 8;
|
||||
int bitPosition = i % 8;
|
||||
|
||||
if (orderedList[i].Value)
|
||||
{
|
||||
byte bitMask = (byte)(1 << bitPosition);
|
||||
request[baseOffset + bytePosition] |= bitMask;
|
||||
}
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public (ushort FirstAddress, ushort NumberOfCoils) DeserializeWriteMultipleCoils(IReadOnlyList<byte> response)
|
||||
{
|
||||
ushort firstAddress = ToNetworkUInt16(response.Skip(8).Take(2).ToArray());
|
||||
ushort numberOfCoils = ToNetworkUInt16(response.Skip(10).Take(2).ToArray());
|
||||
|
||||
return (firstAddress, numberOfCoils);
|
||||
}
|
||||
|
||||
/// <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
|
||||
if (registers == null)
|
||||
throw new ArgumentNullException(nameof(registers));
|
||||
#endif
|
||||
|
||||
var orderedList = registers.OrderBy(c => c.Address).ToList();
|
||||
if (orderedList.Count < MIN_WRITE_COUNT || MAX_REGISTER_WRITE_COUNT < orderedList.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(registers), $"At least {MIN_WRITE_COUNT} or max. {MAX_REGISTER_WRITE_COUNT} holding registers can be written at once.");
|
||||
|
||||
int addrCount = registers.Select(c => c.Address).Distinct().Count();
|
||||
if (orderedList.Count != addrCount)
|
||||
throw new ArgumentException("One or more duplicate holding registers found.", nameof(registers));
|
||||
|
||||
ushort firstAddress = orderedList.First().Address;
|
||||
ushort lastAddress = orderedList.Last().Address;
|
||||
|
||||
if (firstAddress + orderedList.Count - 1 != lastAddress)
|
||||
throw new ArgumentException("Gap in holding register list found.", nameof(registers));
|
||||
|
||||
byte byteCount = (byte)(orderedList.Count * 2);
|
||||
byte[] request = new byte[13 + byteCount];
|
||||
|
||||
byte[] header = GetHeader(unitId, byteCount + 7);
|
||||
Array.Copy(header, 0, request, 0, header.Length);
|
||||
|
||||
request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
|
||||
|
||||
byte[] addrBytes = ToNetworkBytes(firstAddress);
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
byte[] countBytes = ToNetworkBytes((ushort)orderedList.Count);
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
request[12] = byteCount;
|
||||
|
||||
int baseOffset = 13;
|
||||
for (int i = 0; i < orderedList.Count; i++)
|
||||
{
|
||||
request[baseOffset + 2 * i] = orderedList[i].HighByte;
|
||||
request[baseOffset + 2 * i + 1] = orderedList[i].LowByte;
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public (ushort FirstAddress, ushort NumberOfRegisters) DeserializeWriteMultipleHoldingRegisters(IReadOnlyList<byte> response)
|
||||
{
|
||||
ushort firstAddress = ToNetworkUInt16(response.Skip(8).Take(2).ToArray());
|
||||
ushort numberOfRegisters = ToNetworkUInt16(response.Skip(10).Take(2).ToArray());
|
||||
|
||||
return (firstAddress, numberOfRegisters);
|
||||
}
|
||||
|
||||
#endregion Write
|
||||
|
||||
#region Validation
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool CheckResponseComplete(IReadOnlyList<byte> responseBytes)
|
||||
{
|
||||
// 2x Transaction Id
|
||||
// 2x Protocol Identifier
|
||||
// 2x Number of following bytes
|
||||
if (responseBytes.Count < 6)
|
||||
return false;
|
||||
|
||||
ushort followingBytes = ToNetworkUInt16(responseBytes.Skip(4).Take(2).ToArray());
|
||||
if (responseBytes.Count < followingBytes + 6)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ValidateResponse(IReadOnlyList<byte> request, IReadOnlyList<byte> response)
|
||||
{
|
||||
if (!DisableTransactionId)
|
||||
{
|
||||
if (request[0] != response[0] || request[1] != response[1])
|
||||
throw new ModbusException("Transaction Id does not match.");
|
||||
}
|
||||
|
||||
if (request[2] != response[2] || request[3] != response[3])
|
||||
throw new ModbusException("Protocol Identifier does not match.");
|
||||
|
||||
ushort count = ToNetworkUInt16(response.Skip(4).Take(2).ToArray());
|
||||
if (count != response.Count - 6)
|
||||
throw new ModbusException("Number of following bytes does not match.");
|
||||
|
||||
if (request[6] != response[6])
|
||||
throw new ModbusException("Unit Identifier does not match.");
|
||||
|
||||
byte fnCode = response[7];
|
||||
bool isError = (fnCode & 0x80) == 0x80;
|
||||
if (isError)
|
||||
fnCode = (byte)(fnCode ^ 0x80); // === fnCode & 0x7F
|
||||
|
||||
if (request[7] != fnCode)
|
||||
throw new ModbusException("Function code does not match.");
|
||||
|
||||
if (isError)
|
||||
throw new ModbusException("Remote Error") { ErrorCode = (ModbusErrorCode)response[8] };
|
||||
}
|
||||
|
||||
#endregion Validation
|
||||
|
||||
#region Private helpers
|
||||
|
||||
private ushort GetNextTransacitonId()
|
||||
{
|
||||
if (DisableTransactionId)
|
||||
return 0x0000;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_transactionId == ushort.MaxValue)
|
||||
_transactionId = 0x0000;
|
||||
else
|
||||
_transactionId++;
|
||||
|
||||
return _transactionId;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] GetHeader(byte unitId, int followingBytes)
|
||||
{
|
||||
byte[] header = new byte[7];
|
||||
|
||||
// Transaction id
|
||||
ushort txId = GetNextTransacitonId();
|
||||
byte[] txBytes = ToNetworkBytes(txId);
|
||||
header[0] = txBytes[0];
|
||||
header[1] = txBytes[1];
|
||||
|
||||
// Protocol identifier
|
||||
header[2] = 0x00;
|
||||
header[3] = 0x00;
|
||||
|
||||
// Number of following bytes
|
||||
byte[] countBytes = ToNetworkBytes((ushort)followingBytes);
|
||||
header[4] = countBytes[0];
|
||||
header[5] = countBytes[1];
|
||||
|
||||
// Unit identifier
|
||||
header[6] = unitId;
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
private static byte[] ToNetworkBytes(ushort value)
|
||||
{
|
||||
byte[] bytes = BitConverter.GetBytes(value);
|
||||
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(bytes);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static ushort ToNetworkUInt16(byte[] bytes)
|
||||
{
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(bytes);
|
||||
|
||||
return BitConverter.ToUInt16(bytes, 0);
|
||||
}
|
||||
|
||||
#endregion Private helpers
|
||||
}
|
||||
}
|
||||
55
AMWD.Protocols.Modbus.Common/README.md
Normal file
55
AMWD.Protocols.Modbus.Common/README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Modbus Protocol for .NET | Common
|
||||
|
||||
This package contains all basic tools to build your own clients.
|
||||
|
||||
### Contracts
|
||||
|
||||
**IModbusConnection**
|
||||
This is the interface used on the base client to communicate with the remote device.
|
||||
If you want to use a custom connection type, you should implement this interface yourself.
|
||||
|
||||
**IModbusProtocol**
|
||||
If you want to speak a custom type of protocol with the clients, you can implement this interface.
|
||||
|
||||
**ModbusBaseClient**
|
||||
This abstract base client contains all the basic methods and handlings required to communicate via Modbus Protocol.
|
||||
The packages `AMWD.Protocols.Modbus.Serial` _(in progress)_ and `AMWD.Protocols.Modbus.Tcp` have specific derived implementations to match the communication types.
|
||||
|
||||
|
||||
### Enums
|
||||
|
||||
Here you have all typed enumerables defined by the Modbus Protocol.
|
||||
|
||||
|
||||
### Extensions
|
||||
|
||||
To convert the Modbus specific types to usable values and vice-versa, there are some extensions.
|
||||
|
||||
- Decimal extensions for `float` (single) and `double`
|
||||
- Signed extensions for signed integer values as `sbyte`, `short` (int16), `int` (int32) and `long` (int64)
|
||||
- Unsigned extensions for unsigned integer values as `byte`, `ushort` (uint16), `uint` (uint32) and `ulong` (uint64)
|
||||
- Some other extensions for `string` and `bool`
|
||||
|
||||
|
||||
### Models
|
||||
|
||||
The different types handled by the Modbus Protocol.
|
||||
|
||||
- Coil
|
||||
- Discrete Input
|
||||
- Holding Register
|
||||
- Input Register
|
||||
|
||||
|
||||
### Protocols
|
||||
|
||||
Here you have the specific default implementations for the Modbus Protocol.
|
||||
|
||||
- ASCII _(in progress)_
|
||||
- RTU _(in progress)_
|
||||
- TCP
|
||||
|
||||
|
||||
---
|
||||
|
||||
Published under MIT License (see [**tl;dr**Legal](https://www.tldrlegal.com/license/mit-license))
|
||||
Reference in New Issue
Block a user