Implementation of the basic functionallity

This commit is contained in:
2024-02-06 19:47:06 +01:00
parent a6c7828fbe
commit f31f6f94ff
42 changed files with 6875 additions and 11 deletions

View File

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

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

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

View 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");
}
}
}

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

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

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

View 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();
}
}

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

View 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();
}
}

View File

@@ -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]
};
}
}
}
}

View 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;
}
}
}
}

View File

@@ -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]
};
}
}
}
}

View File

@@ -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]
};
}
}
}
}

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("AMWD.Protocols.Modbus.Tests")]

View 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")}";
}
}

View 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")}";
}
}

View 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}";
}
}

View 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}";
}
}

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

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

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