Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c81ab6b44 | |||
| 3e8f2cd73b | |||
| e830e43c36 | |||
| 6a63dbb739 | |||
| 1536c60336 | |||
| 206c5420e1 | |||
| d027b6939a | |||
| 02a724521a | |||
| 2580554bb1 | |||
| bc3ca4fa52 | |||
| 54511c9366 | |||
| 763ca1da25 | |||
| a35c3491ae |
@@ -25,6 +25,8 @@ build-debug:
|
|||||||
- mkdir ./artifacts
|
- mkdir ./artifacts
|
||||||
- mv ./AMWD.Protocols.Modbus.Common/bin/Debug/*.nupkg ./artifacts/
|
- mv ./AMWD.Protocols.Modbus.Common/bin/Debug/*.nupkg ./artifacts/
|
||||||
- mv ./AMWD.Protocols.Modbus.Common/bin/Debug/*.snupkg ./artifacts/
|
- mv ./AMWD.Protocols.Modbus.Common/bin/Debug/*.snupkg ./artifacts/
|
||||||
|
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Debug/*.nupkg ./artifacts/
|
||||||
|
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Debug/*.snupkg ./artifacts/
|
||||||
- mv ./AMWD.Protocols.Modbus.Serial/bin/Debug/*.nupkg ./artifacts/
|
- mv ./AMWD.Protocols.Modbus.Serial/bin/Debug/*.nupkg ./artifacts/
|
||||||
- mv ./AMWD.Protocols.Modbus.Serial/bin/Debug/*.snupkg ./artifacts/
|
- mv ./AMWD.Protocols.Modbus.Serial/bin/Debug/*.snupkg ./artifacts/
|
||||||
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Debug/*.nupkg ./artifacts/
|
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Debug/*.nupkg ./artifacts/
|
||||||
@@ -80,6 +82,8 @@ build-release:
|
|||||||
- mkdir ./artifacts
|
- mkdir ./artifacts
|
||||||
- mv ./AMWD.Protocols.Modbus.Common/bin/Release/*.nupkg ./artifacts/
|
- mv ./AMWD.Protocols.Modbus.Common/bin/Release/*.nupkg ./artifacts/
|
||||||
- mv ./AMWD.Protocols.Modbus.Common/bin/Release/*.snupkg ./artifacts/
|
- mv ./AMWD.Protocols.Modbus.Common/bin/Release/*.snupkg ./artifacts/
|
||||||
|
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Release/*.nupkg ./artifacts/
|
||||||
|
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Release/*.snupkg ./artifacts/
|
||||||
- mv ./AMWD.Protocols.Modbus.Serial/bin/Release/*.nupkg ./artifacts/
|
- mv ./AMWD.Protocols.Modbus.Serial/bin/Release/*.nupkg ./artifacts/
|
||||||
- mv ./AMWD.Protocols.Modbus.Serial/bin/Release/*.snupkg ./artifacts/
|
- mv ./AMWD.Protocols.Modbus.Serial/bin/Release/*.snupkg ./artifacts/
|
||||||
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Release/*.nupkg ./artifacts/
|
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Release/*.nupkg ./artifacts/
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The default protocol used by the client should be initialized in the constructor.
|
/// The default protocol used by the client should be initialized in the constructor.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public abstract IModbusProtocol Protocol { get; set; }
|
public virtual IModbusProtocol Protocol { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads multiple <see cref="Coil"/>s.
|
/// Reads multiple <see cref="Coil"/>s.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("AMWD.Protocols.Modbus.Tests")]
|
[assembly: InternalsVisibleTo("AMWD.Protocols.Modbus.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100adcc4f9f5bb3ac73cb30661f6f35772b8f90a74412925764a960af06ef125bdcec05ed1d139503d5203fb72aa3fa74bab58e82ac2a6cd4b650f8cbf7086a71bc2dfc67e95b8d26d776d60856acf3121f831529b1a4dee91b34ac84f95f71a1165b7783edb591929ba2a684100c92bbed8859c7266fb507f6f55bb6f7fcac80b4")]
|
||||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ using AMWD.Protocols.Modbus.Common.Contracts;
|
|||||||
namespace AMWD.Protocols.Modbus.Common.Protocols
|
namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implementation of the Modbus RTU over TCP protocol.
|
/// Implementation of the Modbus RTU over Modbus TCP protocol.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The Modbus RTU over Modbus TCP is rarely used.
|
/// The Modbus RTU over Modbus TCP is rarely used.
|
||||||
/// It is a non-standard variant of Modbus TCP that includes the Modbus RTU CRC at the end of the message.
|
/// It is a non-standard variant of Modbus TCP that includes wrapps a Modbus RTU message within a Modbus TCP message.
|
||||||
|
/// <br/>
|
||||||
|
/// Definition found on <see href="https://www.fernhillsoftware.com/help/drivers/modbus/modbus-protocol.html">Fernhill Software</see>.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public class RtuOverTcpProtocol : IModbusProtocol
|
public class RtuOverTcpProtocol : IModbusProtocol
|
||||||
{
|
{
|
||||||
@@ -475,16 +477,16 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
// Function code
|
// Function code
|
||||||
request[7] = (byte)ModbusFunctionCode.WriteSingleRegister;
|
request[7] = (byte)ModbusFunctionCode.WriteSingleRegister;
|
||||||
|
|
||||||
byte[] addrBytes = register.Address.ToBigEndianBytes();
|
byte[] addrBytes = register.Address.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
request[10] = register.HighByte;
|
request[10] = register.HighByte;
|
||||||
request[11] = register.LowByte;
|
request[11] = register.LowByte;
|
||||||
|
|
||||||
// CRC
|
// CRC
|
||||||
byte[] crc = RtuProtocol.CRC16(request, 6, 6);
|
byte[] crc = RtuProtocol.CRC16(request, 6, 6);
|
||||||
request[12] = crc[0];
|
request[12] = crc[0];
|
||||||
request[13] = crc[1];
|
request[13] = crc[1];
|
||||||
|
|
||||||
return request;
|
return request;
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ Here you have the specific default implementations for the Modbus Protocol.
|
|||||||
|
|
||||||
- ASCII
|
- ASCII
|
||||||
- RTU
|
- RTU
|
||||||
- RTU over TCP
|
|
||||||
- TCP
|
- TCP
|
||||||
|
- [RTU over TCP]
|
||||||
|
|
||||||
**NOTE:**
|
**NOTE:**
|
||||||
The implementations over serial line (RTU and ASCII) have a minimum unit ID of one (1) and maximum unit ID of 247 referring to the specification.
|
The implementations over serial line (RTU and ASCII) have a minimum unit ID of one (1) and maximum unit ID of 247 referring to the specification.
|
||||||
@@ -68,4 +68,9 @@ This validation is _not_ implemented here due to real world experience, that som
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Published under MIT License (see [**tl;dr**Legal](https://www.tldrlegal.com/license/mit-license))
|
Published under MIT License (see [**tl;dr**Legal])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[RTU over TCP]: https://www.fernhillsoftware.com/help/drivers/modbus/modbus-protocol.html
|
||||||
|
[**tl;dr**Legal]: https://www.tldrlegal.com/license/mit-license
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ namespace System.Collections.Generic
|
|||||||
internalDequeueTcs = ResetToken(ref _dequeueTcs);
|
internalDequeueTcs = ResetToken(ref _dequeueTcs);
|
||||||
}
|
}
|
||||||
|
|
||||||
await WaitAsync(internalDequeueTcs, cancellationToken).ConfigureAwait(false);
|
await WaitAsync(internalDequeueTcs, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ namespace System.Collections.Generic
|
|||||||
{
|
{
|
||||||
if (await Task.WhenAny(tcs.Task, Task.Delay(-1, cancellationToken)) == tcs.Task)
|
if (await Task.WhenAny(tcs.Task, Task.Delay(-1, cancellationToken)) == tcs.Task)
|
||||||
{
|
{
|
||||||
await tcs.Task.ConfigureAwait(false);
|
await tcs.Task;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
||||||
|
<LangVersion>12.0</LangVersion>
|
||||||
|
|
||||||
|
<PackageId>AMWD.Protocols.Modbus.Proxy</PackageId>
|
||||||
|
<AssemblyName>amwd-modbus-proxy</AssemblyName>
|
||||||
|
<RootNamespace>AMWD.Protocols.Modbus.Proxy</RootNamespace>
|
||||||
|
|
||||||
|
<Product>Modbus Proxy Clients</Product>
|
||||||
|
<Description>Plugging Modbus Servers and Clients together to create Modbus Proxies.</Description>
|
||||||
|
<PackageTags>Modbus Protocol Proxy</PackageTags>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" />
|
||||||
|
<Compile Include="../AMWD.Protocols.Modbus.Tcp/Extensions/StreamExtensions.cs" Link="Extensions/StreamExtensions.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="README.md" Pack="true" PackagePath="/" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||||
|
<PackageReference Include="System.IO.Ports" Version="4.7.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
|
||||||
|
<PackageReference Include="System.IO.Ports" Version="6.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||||
|
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
||||||
|
<ProjectReference Include="..\AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
867
AMWD.Protocols.Modbus.Proxy/ModbusRtuProxy.cs
Normal file
867
AMWD.Protocols.Modbus.Proxy/ModbusRtuProxy.cs
Normal file
@@ -0,0 +1,867 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO.Ports;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AMWD.Protocols.Modbus.Common;
|
||||||
|
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||||
|
using AMWD.Protocols.Modbus.Common.Protocols;
|
||||||
|
using AMWD.Protocols.Modbus.Serial;
|
||||||
|
|
||||||
|
namespace AMWD.Protocols.Modbus.Proxy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implements a Modbus serial line RTU server proxying all requests to a Modbus client of choice.
|
||||||
|
/// </summary>
|
||||||
|
public class ModbusRtuProxy : IDisposable
|
||||||
|
{
|
||||||
|
#region Fields
|
||||||
|
|
||||||
|
private bool _isDisposed;
|
||||||
|
|
||||||
|
private readonly SerialPort _serialPort;
|
||||||
|
private CancellationTokenSource _stopCts;
|
||||||
|
|
||||||
|
#endregion Fields
|
||||||
|
|
||||||
|
#region Constructors
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ModbusRtuProxy"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The <see cref="ModbusClientBase"/> used to request the remote device, that should be proxied.</param>
|
||||||
|
/// <param name="portName">The name of the serial port to use.</param>
|
||||||
|
/// <param name="baudRate">The baud rate of the serial port (Default: 19.200).</param>
|
||||||
|
public ModbusRtuProxy(ModbusClientBase client, string portName, BaudRate baudRate = BaudRate.Baud19200)
|
||||||
|
{
|
||||||
|
Client = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(portName))
|
||||||
|
throw new ArgumentNullException(nameof(portName));
|
||||||
|
|
||||||
|
if (!Enum.IsDefined(typeof(BaudRate), baudRate))
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(baudRate));
|
||||||
|
|
||||||
|
if (!ModbusSerialClient.AvailablePortNames.Contains(portName))
|
||||||
|
throw new ArgumentException($"The serial port ({portName}) is not available.", nameof(portName));
|
||||||
|
|
||||||
|
_serialPort = new SerialPort
|
||||||
|
{
|
||||||
|
PortName = portName,
|
||||||
|
BaudRate = (int)baudRate,
|
||||||
|
Handshake = Handshake.None,
|
||||||
|
DataBits = 8,
|
||||||
|
ReadTimeout = 1000,
|
||||||
|
RtsEnable = false,
|
||||||
|
StopBits = StopBits.One,
|
||||||
|
WriteTimeout = 1000,
|
||||||
|
Parity = Parity.Even
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Constructors
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Modbus client used to request the remote device, that should be proxied.
|
||||||
|
/// </summary>
|
||||||
|
public ModbusClientBase Client { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.PortName"/>
|
||||||
|
public string PortName => _serialPort.PortName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the baud rate of the serial port.
|
||||||
|
/// </summary>
|
||||||
|
public BaudRate BaudRate
|
||||||
|
{
|
||||||
|
get => (BaudRate)_serialPort.BaudRate;
|
||||||
|
set => _serialPort.BaudRate = (int)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.Handshake"/>
|
||||||
|
public Handshake Handshake
|
||||||
|
{
|
||||||
|
get => _serialPort.Handshake;
|
||||||
|
set => _serialPort.Handshake = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.DataBits"/>
|
||||||
|
public int DataBits
|
||||||
|
{
|
||||||
|
get => _serialPort.DataBits;
|
||||||
|
set => _serialPort.DataBits = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.IsOpen"/>
|
||||||
|
public bool IsOpen => _serialPort.IsOpen;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a read operation does not finish.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan ReadTimeout
|
||||||
|
{
|
||||||
|
get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout);
|
||||||
|
set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.RtsEnable"/>
|
||||||
|
public bool RtsEnable
|
||||||
|
{
|
||||||
|
get => _serialPort.RtsEnable;
|
||||||
|
set => _serialPort.RtsEnable = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.StopBits"/>
|
||||||
|
public StopBits StopBits
|
||||||
|
{
|
||||||
|
get => _serialPort.StopBits;
|
||||||
|
set => _serialPort.StopBits = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a write operation does not finish.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan WriteTimeout
|
||||||
|
{
|
||||||
|
get => TimeSpan.FromMilliseconds(_serialPort.WriteTimeout);
|
||||||
|
set => _serialPort.WriteTimeout = (int)value.TotalMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.Parity"/>
|
||||||
|
public Parity Parity
|
||||||
|
{
|
||||||
|
get => _serialPort.Parity;
|
||||||
|
set => _serialPort.Parity = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Properties
|
||||||
|
|
||||||
|
#region Control Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the server.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Assertions();
|
||||||
|
|
||||||
|
_stopCts?.Cancel();
|
||||||
|
_serialPort.Close();
|
||||||
|
_serialPort.DataReceived -= OnDataReceived;
|
||||||
|
|
||||||
|
_stopCts?.Dispose();
|
||||||
|
_stopCts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
_serialPort.DataReceived += OnDataReceived;
|
||||||
|
_serialPort.Open();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops the server.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Assertions();
|
||||||
|
return StopAsyncInternal(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task StopAsyncInternal(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_stopCts.Cancel();
|
||||||
|
|
||||||
|
_serialPort.Close();
|
||||||
|
_serialPort.DataReceived -= OnDataReceived;
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases all managed and unmanaged resources used by the <see cref="ModbusRtuProxy"/>.
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
|
||||||
|
StopAsyncInternal(CancellationToken.None).Wait();
|
||||||
|
|
||||||
|
_serialPort.Dispose();
|
||||||
|
_stopCts?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Assertions()
|
||||||
|
{
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
ObjectDisposedException.ThrowIf(_isDisposed, this);
|
||||||
|
#else
|
||||||
|
if (_isDisposed)
|
||||||
|
throw new ObjectDisposedException(GetType().FullName);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Control Methods
|
||||||
|
|
||||||
|
#region Client Handling
|
||||||
|
|
||||||
|
private void OnDataReceived(object _, SerialDataReceivedEventArgs evArgs)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var requestBytes = new List<byte>();
|
||||||
|
do
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[RtuProtocol.MAX_ADU_LENGTH];
|
||||||
|
int count = _serialPort.Read(buffer, 0, buffer.Length);
|
||||||
|
requestBytes.AddRange(buffer.Take(count));
|
||||||
|
|
||||||
|
_stopCts.Token.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
while (_serialPort.BytesToRead > 0);
|
||||||
|
|
||||||
|
_stopCts.Token.ThrowIfCancellationRequested();
|
||||||
|
byte[] responseBytes = HandleRequest([.. requestBytes]);
|
||||||
|
if (responseBytes == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_stopCts.Token.ThrowIfCancellationRequested();
|
||||||
|
_serialPort.Write(responseBytes, 0, responseBytes.Length);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{ /* keep it quiet */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Client Handling
|
||||||
|
|
||||||
|
#region Request Handling
|
||||||
|
|
||||||
|
private byte[] HandleRequest(byte[] requestBytes)
|
||||||
|
{
|
||||||
|
byte[] recvCrc = requestBytes.Skip(requestBytes.Length - 2).ToArray();
|
||||||
|
byte[] calcCrc = RtuProtocol.CRC16(requestBytes, 0, requestBytes.Length - 2);
|
||||||
|
if (!recvCrc.SequenceEqual(calcCrc))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
switch ((ModbusFunctionCode)requestBytes[1])
|
||||||
|
{
|
||||||
|
case ModbusFunctionCode.ReadCoils:
|
||||||
|
return HandleReadCoilsAsync(requestBytes, _stopCts.Token).Result;
|
||||||
|
|
||||||
|
case ModbusFunctionCode.ReadDiscreteInputs:
|
||||||
|
return HandleReadDiscreteInputsAsync(requestBytes, _stopCts.Token).Result;
|
||||||
|
|
||||||
|
case ModbusFunctionCode.ReadHoldingRegisters:
|
||||||
|
return HandleReadHoldingRegistersAsync(requestBytes, _stopCts.Token).Result;
|
||||||
|
|
||||||
|
case ModbusFunctionCode.ReadInputRegisters:
|
||||||
|
return HandleReadInputRegistersAsync(requestBytes, _stopCts.Token).Result;
|
||||||
|
|
||||||
|
case ModbusFunctionCode.WriteSingleCoil:
|
||||||
|
return HandleWriteSingleCoilAsync(requestBytes, _stopCts.Token).Result;
|
||||||
|
|
||||||
|
case ModbusFunctionCode.WriteSingleRegister:
|
||||||
|
return HandleWriteSingleRegisterAsync(requestBytes, _stopCts.Token).Result;
|
||||||
|
|
||||||
|
case ModbusFunctionCode.WriteMultipleCoils:
|
||||||
|
return HandleWriteMultipleCoilsAsync(requestBytes, _stopCts.Token).Result;
|
||||||
|
|
||||||
|
case ModbusFunctionCode.WriteMultipleRegisters:
|
||||||
|
return HandleWriteMultipleRegistersAsync(requestBytes, _stopCts.Token).Result;
|
||||||
|
|
||||||
|
case ModbusFunctionCode.EncapsulatedInterface:
|
||||||
|
return HandleEncapsulatedInterfaceAsync(requestBytes, _stopCts.Token).Result;
|
||||||
|
|
||||||
|
default: // unknown function
|
||||||
|
{
|
||||||
|
byte[] responseBytes = new byte[5];
|
||||||
|
Array.Copy(requestBytes, 0, responseBytes, 0, 2);
|
||||||
|
|
||||||
|
// Mark as error
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
|
||||||
|
responseBytes[2] = (byte)ModbusErrorCode.IllegalFunction;
|
||||||
|
|
||||||
|
SetCrc(responseBytes);
|
||||||
|
return responseBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleReadCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 8)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
byte unitId = requestBytes[0];
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken);
|
||||||
|
|
||||||
|
byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)];
|
||||||
|
for (int i = 0; i < coils.Count; i++)
|
||||||
|
{
|
||||||
|
if (coils[i].Value)
|
||||||
|
{
|
||||||
|
int byteIndex = i / 8;
|
||||||
|
int bitIndex = i % 8;
|
||||||
|
|
||||||
|
values[byteIndex] |= (byte)(1 << bitIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBytes.Add((byte)values.Length);
|
||||||
|
responseBytes.AddRange(values);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddCrc(responseBytes);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 8)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
byte unitId = requestBytes[0];
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken);
|
||||||
|
|
||||||
|
byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)];
|
||||||
|
for (int i = 0; i < discreteInputs.Count; i++)
|
||||||
|
{
|
||||||
|
if (discreteInputs[i].Value)
|
||||||
|
{
|
||||||
|
int byteIndex = i / 8;
|
||||||
|
int bitIndex = i % 8;
|
||||||
|
|
||||||
|
values[byteIndex] |= (byte)(1 << bitIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBytes.Add((byte)values.Length);
|
||||||
|
responseBytes.AddRange(values);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddCrc(responseBytes);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 8)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
byte unitId = requestBytes[0];
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken);
|
||||||
|
|
||||||
|
byte[] values = new byte[holdingRegisters.Count * 2];
|
||||||
|
for (int i = 0; i < holdingRegisters.Count; i++)
|
||||||
|
{
|
||||||
|
values[i * 2] = holdingRegisters[i].HighByte;
|
||||||
|
values[i * 2 + 1] = holdingRegisters[i].LowByte;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBytes.Add((byte)values.Length);
|
||||||
|
responseBytes.AddRange(values);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddCrc(responseBytes);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 8)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
byte unitId = requestBytes[0];
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken);
|
||||||
|
|
||||||
|
byte[] values = new byte[count * 2];
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
values[i * 2] = inputRegisters[i].HighByte;
|
||||||
|
values[i * 2 + 1] = inputRegisters[i].LowByte;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBytes.Add((byte)values.Length);
|
||||||
|
responseBytes.AddRange(values);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddCrc(responseBytes);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 8)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
|
|
||||||
|
ushort address = requestBytes.GetBigEndianUInt16(2);
|
||||||
|
|
||||||
|
if (requestBytes[4] != 0x00 && requestBytes[4] != 0xFF)
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
|
||||||
|
AddCrc(responseBytes);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var coil = new Coil
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
HighByte = requestBytes[4],
|
||||||
|
LowByte = requestBytes[5],
|
||||||
|
};
|
||||||
|
|
||||||
|
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[0], coil, cancellationToken);
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
// Response is an echo of the request
|
||||||
|
responseBytes.AddRange(requestBytes.Skip(2).Take(4));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddCrc(responseBytes);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 8)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
|
|
||||||
|
ushort address = requestBytes.GetBigEndianUInt16(2);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var register = new HoldingRegister
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
HighByte = requestBytes[4],
|
||||||
|
LowByte = requestBytes[5]
|
||||||
|
};
|
||||||
|
|
||||||
|
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[0], register, cancellationToken);
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
// Response is an echo of the request
|
||||||
|
responseBytes.AddRange(requestBytes.Skip(2).Take(4));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddCrc(responseBytes);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 9)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
|
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
|
|
||||||
|
int byteCount = (int)Math.Ceiling(count / 8.0);
|
||||||
|
if (requestBytes.Length < 9 + byteCount)
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
|
||||||
|
AddCrc(responseBytes);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int baseOffset = 7;
|
||||||
|
var coils = new List<Coil>();
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
int bytePosition = i / 8;
|
||||||
|
int bitPosition = i % 8;
|
||||||
|
|
||||||
|
ushort address = (ushort)(firstAddress + i);
|
||||||
|
bool value = (requestBytes[baseOffset + bytePosition] & (1 << bitPosition)) > 0;
|
||||||
|
|
||||||
|
coils.Add(new Coil
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
HighByte = value ? (byte)0xFF : (byte)0x00
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[0], coils, cancellationToken);
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
// Response is an echo of the request
|
||||||
|
responseBytes.AddRange(requestBytes.Skip(2).Take(4));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddCrc(responseBytes);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 9)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
|
|
||||||
|
int byteCount = count * 2;
|
||||||
|
if (requestBytes.Length < 9 + byteCount)
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
|
||||||
|
AddCrc(responseBytes);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int baseOffset = 7;
|
||||||
|
var list = new List<HoldingRegister>();
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
ushort address = (ushort)(firstAddress + i);
|
||||||
|
|
||||||
|
list.Add(new HoldingRegister
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
HighByte = requestBytes[baseOffset + i * 2],
|
||||||
|
LowByte = requestBytes[baseOffset + i * 2 + 1]
|
||||||
|
});
|
||||||
|
|
||||||
|
bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[0], list, cancellationToken);
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
// Response is an echo of the request
|
||||||
|
responseBytes.AddRange(requestBytes.Skip(2).Take(4));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddCrc(responseBytes);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
|
|
||||||
|
if (requestBytes[2] != 0x0E)
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
|
||||||
|
|
||||||
|
AddCrc(responseBytes);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstObject = (ModbusDeviceIdentificationObject)requestBytes[4];
|
||||||
|
if (0x06 < requestBytes[4] && requestBytes[4] < 0x80)
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
|
||||||
|
|
||||||
|
AddCrc(responseBytes);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
var category = (ModbusDeviceIdentificationCategory)requestBytes[3];
|
||||||
|
if (!Enum.IsDefined(typeof(ModbusDeviceIdentificationCategory), category))
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
|
||||||
|
AddCrc(responseBytes);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var res = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken);
|
||||||
|
|
||||||
|
var bodyBytes = new List<byte>();
|
||||||
|
|
||||||
|
// MEI, Category
|
||||||
|
bodyBytes.AddRange(requestBytes.Skip(2).Take(2));
|
||||||
|
|
||||||
|
// Conformity
|
||||||
|
bodyBytes.Add((byte)category);
|
||||||
|
if (res.IsIndividualAccessAllowed)
|
||||||
|
bodyBytes[2] |= 0x80;
|
||||||
|
|
||||||
|
// More, NextId, NumberOfObjects
|
||||||
|
bodyBytes.AddRange(new byte[3]);
|
||||||
|
|
||||||
|
int maxObjectId;
|
||||||
|
switch (category)
|
||||||
|
{
|
||||||
|
case ModbusDeviceIdentificationCategory.Basic:
|
||||||
|
maxObjectId = 0x02;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationCategory.Regular:
|
||||||
|
maxObjectId = 0x06;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationCategory.Extended:
|
||||||
|
maxObjectId = 0xFF;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: // Individual
|
||||||
|
maxObjectId = requestBytes[4];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte numberOfObjects = 0;
|
||||||
|
for (int i = requestBytes[4]; i <= maxObjectId; i++)
|
||||||
|
{
|
||||||
|
// Reserved
|
||||||
|
if (0x07 <= i && i <= 0x7F)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
byte[] objBytes = GetDeviceObject((byte)i, res);
|
||||||
|
|
||||||
|
// We need to split the response if it would exceed the max ADU size
|
||||||
|
if (responseBytes.Count + bodyBytes.Count + objBytes.Length > RtuProtocol.MAX_ADU_LENGTH)
|
||||||
|
{
|
||||||
|
bodyBytes[3] = 0xFF;
|
||||||
|
bodyBytes[4] = (byte)i;
|
||||||
|
|
||||||
|
bodyBytes[5] = numberOfObjects;
|
||||||
|
responseBytes.AddRange(bodyBytes);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes.AddRange(objBytes);
|
||||||
|
numberOfObjects++;
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes[5] = numberOfObjects;
|
||||||
|
responseBytes.AddRange(bodyBytes);
|
||||||
|
|
||||||
|
AddCrc(responseBytes);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
|
||||||
|
AddCrc(responseBytes);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] GetDeviceObject(byte objectId, DeviceIdentification deviceIdentification)
|
||||||
|
{
|
||||||
|
var result = new List<byte> { objectId };
|
||||||
|
switch ((ModbusDeviceIdentificationObject)objectId)
|
||||||
|
{
|
||||||
|
case ModbusDeviceIdentificationObject.VendorName:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName);
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.ProductCode:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode);
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.MajorMinorRevision:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision);
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.VendorUrl:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl);
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.ProductName:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName);
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.ModelName:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName);
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.UserApplicationName:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName);
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
if (deviceIdentification.ExtendedObjects.ContainsKey(objectId))
|
||||||
|
{
|
||||||
|
byte[] bytes = deviceIdentification.ExtendedObjects[objectId];
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.Add(0x00);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [.. result];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetCrc(byte[] bytes)
|
||||||
|
{
|
||||||
|
byte[] crc = RtuProtocol.CRC16(bytes, 0, bytes.Length - 2);
|
||||||
|
bytes[bytes.Length - 2] = crc[0];
|
||||||
|
bytes[bytes.Length - 1] = crc[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddCrc(List<byte> bytes)
|
||||||
|
{
|
||||||
|
byte[] crc = RtuProtocol.CRC16(bytes);
|
||||||
|
bytes.Add(crc[0]);
|
||||||
|
bytes.Add(crc[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Request Handling
|
||||||
|
}
|
||||||
|
}
|
||||||
855
AMWD.Protocols.Modbus.Proxy/ModbusTcpProxy.cs
Normal file
855
AMWD.Protocols.Modbus.Proxy/ModbusTcpProxy.cs
Normal file
@@ -0,0 +1,855 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AMWD.Protocols.Modbus.Common;
|
||||||
|
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||||
|
using AMWD.Protocols.Modbus.Common.Protocols;
|
||||||
|
|
||||||
|
namespace AMWD.Protocols.Modbus.Proxy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implements a Modbus TCP server proxying all requests to a Modbus client of choice.
|
||||||
|
/// </summary>
|
||||||
|
public class ModbusTcpProxy : IDisposable
|
||||||
|
{
|
||||||
|
#region Fields
|
||||||
|
|
||||||
|
private bool _isDisposed;
|
||||||
|
|
||||||
|
private TcpListener _listener;
|
||||||
|
private CancellationTokenSource _stopCts;
|
||||||
|
private Task _clientConnectTask = Task.CompletedTask;
|
||||||
|
|
||||||
|
private readonly SemaphoreSlim _clientListLock = new(1, 1);
|
||||||
|
private readonly List<TcpClient> _clients = [];
|
||||||
|
private readonly List<Task> _clientTasks = [];
|
||||||
|
|
||||||
|
#endregion Fields
|
||||||
|
|
||||||
|
#region Constructors
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ModbusTcpProxy"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The <see cref="ModbusClientBase"/> used to request the remote device, that should be proxied.</param>
|
||||||
|
/// <param name="listenAddress">An <see cref="IPAddress"/> to listen on (Default: <see cref="IPAddress.Loopback"/>).</param>
|
||||||
|
/// <param name="listenPort">A port to listen on (Default: 502).</param>
|
||||||
|
public ModbusTcpProxy(ModbusClientBase client, IPAddress listenAddress = null, int listenPort = 502)
|
||||||
|
{
|
||||||
|
Client = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
|
||||||
|
ListenAddress = listenAddress ?? IPAddress.Loopback;
|
||||||
|
|
||||||
|
if (listenPort < ushort.MinValue || ushort.MaxValue < listenPort)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(listenPort));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
using var testListener = new TcpListener(ListenAddress, listenPort);
|
||||||
|
#else
|
||||||
|
var testListener = new TcpListener(ListenAddress, listenPort);
|
||||||
|
#endif
|
||||||
|
testListener.Start(1);
|
||||||
|
ListenPort = (testListener.LocalEndpoint as IPEndPoint).Port;
|
||||||
|
testListener.Stop();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"{nameof(ListenPort)} ({listenPort}) is already in use.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Constructors
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Modbus client used to request the remote device, that should be proxied.
|
||||||
|
/// </summary>
|
||||||
|
public ModbusClientBase Client { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="IPAddress"/> to listen on.
|
||||||
|
/// </summary>
|
||||||
|
public IPAddress ListenAddress { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the port to listen on.
|
||||||
|
/// </summary>
|
||||||
|
public int ListenPort { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the server is running.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRunning => _listener?.Server.IsBound ?? false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the read/write timeout for the incoming connections (not the <see cref="Client"/>!).
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan ReadWriteTimeout { get; set; }
|
||||||
|
|
||||||
|
#endregion Properties
|
||||||
|
|
||||||
|
#region Control Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the server.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Assertions();
|
||||||
|
|
||||||
|
_stopCts?.Cancel();
|
||||||
|
|
||||||
|
_listener?.Stop();
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
_listener?.Dispose();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
_stopCts?.Dispose();
|
||||||
|
_stopCts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
_listener = new TcpListener(ListenAddress, ListenPort);
|
||||||
|
if (ListenAddress.AddressFamily == AddressFamily.InterNetworkV6)
|
||||||
|
_listener.Server.DualMode = true;
|
||||||
|
|
||||||
|
_listener.Start();
|
||||||
|
_clientConnectTask = WaitForClientAsync(_stopCts.Token);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops the server.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Assertions();
|
||||||
|
return StopAsyncInternal(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StopAsyncInternal(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_stopCts.Cancel();
|
||||||
|
|
||||||
|
_listener.Stop();
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
_listener.Dispose();
|
||||||
|
#endif
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.WhenAny(_clientConnectTask, Task.Delay(Timeout.Infinite, cancellationToken));
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Terminated
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.WhenAny(Task.WhenAll(_clientTasks), Task.Delay(Timeout.Infinite, cancellationToken));
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Terminated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases all managed and unmanaged resources used by the <see cref="ModbusTcpProxy"/>.
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
|
||||||
|
StopAsyncInternal(CancellationToken.None).Wait();
|
||||||
|
|
||||||
|
_clientListLock.Dispose();
|
||||||
|
_clients.Clear();
|
||||||
|
|
||||||
|
_stopCts?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Assertions()
|
||||||
|
{
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
ObjectDisposedException.ThrowIf(_isDisposed, this);
|
||||||
|
#else
|
||||||
|
if (_isDisposed)
|
||||||
|
throw new ObjectDisposedException(GetType().FullName);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Control Methods
|
||||||
|
|
||||||
|
#region Client Handling
|
||||||
|
|
||||||
|
private async Task WaitForClientAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
var client = await _listener.AcceptTcpClientAsync(cancellationToken);
|
||||||
|
#else
|
||||||
|
var client = await _listener.AcceptTcpClientAsync();
|
||||||
|
#endif
|
||||||
|
await _clientListLock.WaitAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_clients.Add(client);
|
||||||
|
_clientTasks.Add(HandleClientAsync(client, cancellationToken));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_clientListLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// There might be a failure here, that's ok, just keep it quiet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var stream = client.GetStream();
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var requestBytes = new List<byte>();
|
||||||
|
|
||||||
|
using (var cts = new CancellationTokenSource(ReadWriteTimeout))
|
||||||
|
using (cancellationToken.Register(cts.Cancel))
|
||||||
|
{
|
||||||
|
byte[] headerBytes = await stream.ReadExpectedBytesAsync(6, cts.Token);
|
||||||
|
requestBytes.AddRange(headerBytes);
|
||||||
|
|
||||||
|
byte[] followingCountBytes = headerBytes.Skip(4).Take(2).ToArray();
|
||||||
|
followingCountBytes.SwapBigEndian();
|
||||||
|
int followingCount = BitConverter.ToUInt16(followingCountBytes, 0);
|
||||||
|
|
||||||
|
byte[] bodyBytes = await stream.ReadExpectedBytesAsync(followingCount, cts.Token);
|
||||||
|
requestBytes.AddRange(bodyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] responseBytes = await HandleRequestAsync([.. requestBytes], cancellationToken);
|
||||||
|
if (responseBytes != null)
|
||||||
|
await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Keep client processing quiet
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await _clientListLock.WaitAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_clients.Remove(client);
|
||||||
|
client.Dispose();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_clientListLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Client Handling
|
||||||
|
|
||||||
|
#region Request Handling
|
||||||
|
|
||||||
|
private Task<byte[]> HandleRequestAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
switch ((ModbusFunctionCode)requestBytes[7])
|
||||||
|
{
|
||||||
|
case ModbusFunctionCode.ReadCoils:
|
||||||
|
return HandleReadCoilsAsync(requestBytes, cancellationToken);
|
||||||
|
|
||||||
|
case ModbusFunctionCode.ReadDiscreteInputs:
|
||||||
|
return HandleReadDiscreteInputsAsync(requestBytes, cancellationToken);
|
||||||
|
|
||||||
|
case ModbusFunctionCode.ReadHoldingRegisters:
|
||||||
|
return HandleReadHoldingRegistersAsync(requestBytes, cancellationToken);
|
||||||
|
|
||||||
|
case ModbusFunctionCode.ReadInputRegisters:
|
||||||
|
return HandleReadInputRegistersAsync(requestBytes, cancellationToken);
|
||||||
|
|
||||||
|
case ModbusFunctionCode.WriteSingleCoil:
|
||||||
|
return HandleWriteSingleCoilAsync(requestBytes, cancellationToken);
|
||||||
|
|
||||||
|
case ModbusFunctionCode.WriteSingleRegister:
|
||||||
|
return HandleWriteSingleRegisterAsync(requestBytes, cancellationToken);
|
||||||
|
|
||||||
|
case ModbusFunctionCode.WriteMultipleCoils:
|
||||||
|
return HandleWriteMultipleCoilsAsync(requestBytes, cancellationToken);
|
||||||
|
|
||||||
|
case ModbusFunctionCode.WriteMultipleRegisters:
|
||||||
|
return HandleWriteMultipleRegistersAsync(requestBytes, cancellationToken);
|
||||||
|
|
||||||
|
case ModbusFunctionCode.EncapsulatedInterface:
|
||||||
|
return HandleEncapsulatedInterfaceAsync(requestBytes, cancellationToken);
|
||||||
|
|
||||||
|
default: // unknown function
|
||||||
|
{
|
||||||
|
byte[] responseBytes = new byte[9];
|
||||||
|
Array.Copy(requestBytes, 0, responseBytes, 0, 8);
|
||||||
|
|
||||||
|
// Mark as error
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
|
||||||
|
responseBytes[8] = (byte)ModbusErrorCode.IllegalFunction;
|
||||||
|
return Task.FromResult(responseBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleReadCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 12)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
byte unitId = requestBytes[6];
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(10);
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken);
|
||||||
|
|
||||||
|
byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)];
|
||||||
|
for (int i = 0; i < coils.Count; i++)
|
||||||
|
{
|
||||||
|
if (coils[i].Value)
|
||||||
|
{
|
||||||
|
int byteIndex = i / 8;
|
||||||
|
int bitIndex = i % 8;
|
||||||
|
|
||||||
|
values[byteIndex] |= (byte)(1 << bitIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBytes.Add((byte)values.Length);
|
||||||
|
responseBytes.AddRange(values);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 12)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
byte unitId = requestBytes[6];
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(10);
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken);
|
||||||
|
|
||||||
|
byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)];
|
||||||
|
for (int i = 0; i < discreteInputs.Count; i++)
|
||||||
|
{
|
||||||
|
if (discreteInputs[i].Value)
|
||||||
|
{
|
||||||
|
int byteIndex = i / 8;
|
||||||
|
int bitIndex = i % 8;
|
||||||
|
|
||||||
|
values[byteIndex] |= (byte)(1 << bitIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBytes.Add((byte)values.Length);
|
||||||
|
responseBytes.AddRange(values);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 12)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
byte unitId = requestBytes[6];
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(10);
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken);
|
||||||
|
|
||||||
|
byte[] values = new byte[holdingRegisters.Count * 2];
|
||||||
|
for (int i = 0; i < holdingRegisters.Count; i++)
|
||||||
|
{
|
||||||
|
values[i * 2] = holdingRegisters[i].HighByte;
|
||||||
|
values[i * 2 + 1] = holdingRegisters[i].LowByte;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBytes.Add((byte)values.Length);
|
||||||
|
responseBytes.AddRange(values);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 12)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
byte unitId = requestBytes[6];
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(10);
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken);
|
||||||
|
|
||||||
|
byte[] values = new byte[count * 2];
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
values[i * 2] = inputRegisters[i].HighByte;
|
||||||
|
values[i * 2 + 1] = inputRegisters[i].LowByte;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBytes.Add((byte)values.Length);
|
||||||
|
responseBytes.AddRange(values);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 12)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
|
||||||
|
ushort address = requestBytes.GetBigEndianUInt16(8);
|
||||||
|
|
||||||
|
if (requestBytes[10] != 0x00 && requestBytes[10] != 0xFF)
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var coil = new Coil
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
HighByte = requestBytes[10],
|
||||||
|
LowByte = requestBytes[11],
|
||||||
|
};
|
||||||
|
|
||||||
|
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[6], coil, cancellationToken);
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
// Response is an echo of the request
|
||||||
|
responseBytes.AddRange(requestBytes.Skip(8).Take(4));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 12)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
|
||||||
|
ushort address = requestBytes.GetBigEndianUInt16(8);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var register = new HoldingRegister
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
HighByte = requestBytes[10],
|
||||||
|
LowByte = requestBytes[11]
|
||||||
|
};
|
||||||
|
|
||||||
|
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[6], register, cancellationToken);
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
// Response is an echo of the request
|
||||||
|
responseBytes.AddRange(requestBytes.Skip(8).Take(4));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 13)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(10);
|
||||||
|
|
||||||
|
int byteCount = (int)Math.Ceiling(count / 8.0);
|
||||||
|
if (requestBytes.Length < 13 + byteCount)
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int baseOffset = 13;
|
||||||
|
var coils = new List<Coil>();
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
int bytePosition = i / 8;
|
||||||
|
int bitPosition = i % 8;
|
||||||
|
|
||||||
|
ushort address = (ushort)(firstAddress + i);
|
||||||
|
bool value = (requestBytes[baseOffset + bytePosition] & (1 << bitPosition)) > 0;
|
||||||
|
|
||||||
|
coils.Add(new Coil
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
HighByte = value ? (byte)0xFF : (byte)0x00
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[6], coils, cancellationToken);
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
// Response is an echo of the request
|
||||||
|
responseBytes.AddRange(requestBytes.Skip(8).Take(4));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 13)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(10);
|
||||||
|
|
||||||
|
int byteCount = count * 2;
|
||||||
|
if (requestBytes.Length < 13 + byteCount)
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int baseOffset = 13;
|
||||||
|
var list = new List<HoldingRegister>();
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
ushort address = (ushort)(firstAddress + i);
|
||||||
|
|
||||||
|
list.Add(new HoldingRegister
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
HighByte = requestBytes[baseOffset + i * 2],
|
||||||
|
LowByte = requestBytes[baseOffset + i * 2 + 1]
|
||||||
|
});
|
||||||
|
|
||||||
|
bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[6], list, cancellationToken);
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
// Response is an echo of the request
|
||||||
|
responseBytes.AddRange(requestBytes.Skip(8).Take(4));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
|
||||||
|
if (requestBytes[8] != 0x0E)
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstObject = (ModbusDeviceIdentificationObject)requestBytes[10];
|
||||||
|
if (0x06 < requestBytes[10] && requestBytes[10] < 0x80)
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
var category = (ModbusDeviceIdentificationCategory)requestBytes[9];
|
||||||
|
if (!Enum.IsDefined(typeof(ModbusDeviceIdentificationCategory), category))
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var res = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken);
|
||||||
|
|
||||||
|
var bodyBytes = new List<byte>();
|
||||||
|
|
||||||
|
// MEI, Category
|
||||||
|
bodyBytes.AddRange(requestBytes.Skip(8).Take(2));
|
||||||
|
|
||||||
|
// Conformity
|
||||||
|
bodyBytes.Add((byte)category);
|
||||||
|
if (res.IsIndividualAccessAllowed)
|
||||||
|
bodyBytes[2] |= 0x80;
|
||||||
|
|
||||||
|
// More, NextId, NumberOfObjects
|
||||||
|
bodyBytes.AddRange(new byte[3]);
|
||||||
|
|
||||||
|
int maxObjectId;
|
||||||
|
switch (category)
|
||||||
|
{
|
||||||
|
case ModbusDeviceIdentificationCategory.Basic:
|
||||||
|
maxObjectId = 0x02;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationCategory.Regular:
|
||||||
|
maxObjectId = 0x06;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationCategory.Extended:
|
||||||
|
maxObjectId = 0xFF;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: // Individual
|
||||||
|
maxObjectId = requestBytes[10];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte numberOfObjects = 0;
|
||||||
|
for (int i = requestBytes[10]; i <= maxObjectId; i++)
|
||||||
|
{
|
||||||
|
// Reserved
|
||||||
|
if (0x07 <= i && i <= 0x7F)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
byte[] objBytes = GetDeviceObject((byte)i, res);
|
||||||
|
|
||||||
|
// We need to split the response if it would exceed the max ADU size
|
||||||
|
if (responseBytes.Count + bodyBytes.Count + objBytes.Length > TcpProtocol.MAX_ADU_LENGTH)
|
||||||
|
{
|
||||||
|
bodyBytes[3] = 0xFF;
|
||||||
|
bodyBytes[4] = (byte)i;
|
||||||
|
|
||||||
|
bodyBytes[5] = numberOfObjects;
|
||||||
|
responseBytes.AddRange(bodyBytes);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes.AddRange(objBytes);
|
||||||
|
numberOfObjects++;
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes[5] = numberOfObjects;
|
||||||
|
responseBytes.AddRange(bodyBytes);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
return [.. responseBytes];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] GetDeviceObject(byte objectId, DeviceIdentification deviceIdentification)
|
||||||
|
{
|
||||||
|
var result = new List<byte> { objectId };
|
||||||
|
switch ((ModbusDeviceIdentificationObject)objectId)
|
||||||
|
{
|
||||||
|
case ModbusDeviceIdentificationObject.VendorName:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName);
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.ProductCode:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode);
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.MajorMinorRevision:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision);
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.VendorUrl:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl);
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.ProductName:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName);
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.ModelName:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName);
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.UserApplicationName:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName);
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
if (deviceIdentification.ExtendedObjects.ContainsKey(objectId))
|
||||||
|
{
|
||||||
|
byte[] bytes = deviceIdentification.ExtendedObjects[objectId];
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.Add(0x00);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [.. result];
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Request Handling
|
||||||
|
}
|
||||||
|
}
|
||||||
10
AMWD.Protocols.Modbus.Proxy/README.md
Normal file
10
AMWD.Protocols.Modbus.Proxy/README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Modbus Protocol for .NET | Proxy
|
||||||
|
|
||||||
|
With this package the server and client implementations will be combined as proxy.
|
||||||
|
|
||||||
|
You can use any `ModbusBaseClient` implementation as target client and plug it into the implemented `ModbusTcpProxy` or `ModbusRtuProxy`, which implement the server side.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Published under MIT License (see [**tl;dr**Legal](https://www.tldrlegal.com/license/mit-license))
|
||||||
@@ -41,8 +41,4 @@
|
|||||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Extensions\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ using AMWD.Protocols.Modbus.Common.Protocols;
|
|||||||
namespace AMWD.Protocols.Modbus.Serial
|
namespace AMWD.Protocols.Modbus.Serial
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A basic implementation of a Modbus serial line server.
|
/// A basic implementation of a Modbus serial line RTU server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
public class ModbusSerialServer : IDisposable
|
public class ModbusRtuServer : IDisposable
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
|
|
||||||
@@ -34,11 +34,11 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
#region Constructors
|
#region Constructors
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ModbusSerialServer"/> class.
|
/// Initializes a new instance of the <see cref="ModbusRtuServer"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="portName">The name of the serial port to use.</param>
|
/// <param name="portName">The name of the serial port to use.</param>
|
||||||
/// <param name="baudRate">The baud rate of the serial port (Default: 19.200).</param>
|
/// <param name="baudRate">The baud rate of the serial port (Default: 19.200).</param>
|
||||||
public ModbusSerialServer(string portName, BaudRate baudRate = BaudRate.Baud19200)
|
public ModbusRtuServer(string portName, BaudRate baudRate = BaudRate.Baud19200)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(portName))
|
if (string.IsNullOrWhiteSpace(portName))
|
||||||
throw new ArgumentNullException(nameof(portName));
|
throw new ArgumentNullException(nameof(portName));
|
||||||
@@ -186,7 +186,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
|
|
||||||
private Task StopAsyncInternal(CancellationToken cancellationToken)
|
private Task StopAsyncInternal(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_stopCts.Cancel();
|
_stopCts?.Cancel();
|
||||||
|
|
||||||
_serialPort.Close();
|
_serialPort.Close();
|
||||||
_serialPort.DataReceived -= OnDataReceived;
|
_serialPort.DataReceived -= OnDataReceived;
|
||||||
@@ -195,7 +195,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Releases all managed and unmanaged resources used by the <see cref="ModbusSerialServer"/>.
|
/// Releases all managed and unmanaged resources used by the <see cref="ModbusRtuServer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
@@ -208,6 +208,9 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
|
|
||||||
_deviceListLock.Dispose();
|
_deviceListLock.Dispose();
|
||||||
_devices.Clear();
|
_devices.Clear();
|
||||||
|
|
||||||
|
_serialPort.Dispose();
|
||||||
|
_stopCts?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Assertions()
|
private void Assertions()
|
||||||
@@ -325,7 +328,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
ushort count = requestBytes.GetBigEndianUInt16(4);
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
|
|
||||||
if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_DISCRETE_READ_COUNT)
|
if (count < RtuProtocol.MIN_READ_COUNT || RtuProtocol.MAX_DISCRETE_READ_COUNT < count)
|
||||||
{
|
{
|
||||||
responseBytes[1] |= 0x80;
|
responseBytes[1] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
@@ -382,7 +385,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
ushort count = requestBytes.GetBigEndianUInt16(4);
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
|
|
||||||
if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_DISCRETE_READ_COUNT)
|
if (count < RtuProtocol.MIN_READ_COUNT || RtuProtocol.MAX_DISCRETE_READ_COUNT < count)
|
||||||
{
|
{
|
||||||
responseBytes[1] |= 0x80;
|
responseBytes[1] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
@@ -439,7 +442,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
ushort count = requestBytes.GetBigEndianUInt16(4);
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
|
|
||||||
if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_REGISTER_READ_COUNT)
|
if (count < RtuProtocol.MIN_READ_COUNT || RtuProtocol.MAX_REGISTER_READ_COUNT < count)
|
||||||
{
|
{
|
||||||
responseBytes[1] |= 0x80;
|
responseBytes[1] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
@@ -493,7 +496,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
ushort count = requestBytes.GetBigEndianUInt16(4);
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
|
|
||||||
if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_REGISTER_READ_COUNT)
|
if (count < RtuProtocol.MIN_READ_COUNT || RtuProtocol.MAX_REGISTER_READ_COUNT < count)
|
||||||
{
|
{
|
||||||
responseBytes[1] |= 0x80;
|
responseBytes[1] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
@@ -43,9 +43,6 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
/// <inheritdoc cref="SerialPort.GetPortNames" />
|
/// <inheritdoc cref="SerialPort.GetPortNames" />
|
||||||
public static string[] AvailablePortNames => SerialPort.GetPortNames();
|
public static string[] AvailablePortNames => SerialPort.GetPortNames();
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override IModbusProtocol Protocol { get; set; }
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IModbusConnection.IdleTimeout"/>
|
/// <inheritdoc cref="IModbusConnection.IdleTimeout"/>
|
||||||
public TimeSpan IdleTimeout
|
public TimeSpan IdleTimeout
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Get next request to process
|
// Get next request to process
|
||||||
var item = await _requestQueue.DequeueAsync(cancellationToken).ConfigureAwait(false);
|
var item = await _requestQueue.DequeueAsync(cancellationToken);
|
||||||
|
|
||||||
// Remove registration => already removed from queue
|
// Remove registration => already removed from queue
|
||||||
item.CancellationTokenRegistration.Dispose();
|
item.CancellationTokenRegistration.Dispose();
|
||||||
@@ -267,13 +267,13 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
// Build combined cancellation token
|
// Build combined cancellation token
|
||||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
|
||||||
// Wait for exclusive access
|
// Wait for exclusive access
|
||||||
await _portLock.WaitAsync(linkedCts.Token).ConfigureAwait(false);
|
await _portLock.WaitAsync(linkedCts.Token);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Ensure connection is up
|
// Ensure connection is up
|
||||||
await AssertConnection(linkedCts.Token).ConfigureAwait(false);
|
await AssertConnection(linkedCts.Token);
|
||||||
|
|
||||||
await _serialPort.WriteAsync(item.Request, linkedCts.Token).ConfigureAwait(false);
|
await _serialPort.WriteAsync(item.Request, linkedCts.Token);
|
||||||
|
|
||||||
linkedCts.Token.ThrowIfCancellationRequested();
|
linkedCts.Token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
@@ -282,7 +282,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
int readCount = await _serialPort.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token).ConfigureAwait(false);
|
int readCount = await _serialPort.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token);
|
||||||
if (readCount < 1)
|
if (readCount < 1)
|
||||||
throw new EndOfStreamException();
|
throw new EndOfStreamException();
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
_portLock.Release();
|
_portLock.Release();
|
||||||
_idleTimer.Change(IdleTimeout, Timeout.InfiniteTimeSpan);
|
_idleTimer.Change(IdleTimeout, Timeout.InfiniteTimeSpan);
|
||||||
|
|
||||||
await Task.Delay(InterRequestDelay, cancellationToken).ConfigureAwait(false);
|
await Task.Delay(InterRequestDelay, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
@@ -370,7 +370,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(false);
|
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{ /* keep it quiet */ }
|
{ /* keep it quiet */ }
|
||||||
|
|||||||
@@ -22,6 +22,19 @@ float voltage = registers.GetSingle();
|
|||||||
Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V");
|
Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V");
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you want to use the `ASCII` protocol instead, you can do this on initialization:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// [...]
|
||||||
|
|
||||||
|
using var client = new ModbusSerialClient(serialPort)
|
||||||
|
{
|
||||||
|
Protocol = new AsciiProtocol();
|
||||||
|
};
|
||||||
|
|
||||||
|
// [...]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Sources
|
## Sources
|
||||||
|
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await _serialPort.BaseStream.ReadAsync(buffer, offset, count, cts.Token).ConfigureAwait(false);
|
return await _serialPort.BaseStream.ReadAsync(buffer, offset, count, cts.Token);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@@ -195,9 +195,9 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
#if NET6_0_OR_GREATER
|
#if NET6_0_OR_GREATER
|
||||||
await _serialPort.BaseStream.WriteAsync(buffer, cts.Token).ConfigureAwait(false);
|
await _serialPort.BaseStream.WriteAsync(buffer, cts.Token);
|
||||||
#else
|
#else
|
||||||
await _serialPort.BaseStream.WriteAsync(buffer, 0, buffer.Length, cts.Token).ConfigureAwait(false);
|
await _serialPort.BaseStream.WriteAsync(buffer, 0, buffer.Length, cts.Token);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace System.IO
|
|||||||
int offset = 0;
|
int offset = 0;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken).ConfigureAwait(false);
|
int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken);
|
||||||
if (count < 1)
|
if (count < 1)
|
||||||
throw new EndOfStreamException();
|
throw new EndOfStreamException();
|
||||||
|
|
||||||
|
|||||||
@@ -40,9 +40,6 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
Protocol = new TcpProtocol();
|
Protocol = new TcpProtocol();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override IModbusProtocol Protocol { get; set; }
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IModbusConnection.IdleTimeout"/>
|
/// <inheritdoc cref="IModbusConnection.IdleTimeout"/>
|
||||||
public TimeSpan IdleTimeout
|
public TimeSpan IdleTimeout
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -26,13 +26,17 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
private bool _isDisposed;
|
private bool _isDisposed;
|
||||||
private readonly CancellationTokenSource _disposeCts = new();
|
private readonly CancellationTokenSource _disposeCts = new();
|
||||||
|
|
||||||
|
private readonly TcpClientWrapperFactory _tcpClientFactory = new();
|
||||||
private readonly SemaphoreSlim _clientLock = new(1, 1);
|
private readonly SemaphoreSlim _clientLock = new(1, 1);
|
||||||
private readonly TcpClientWrapper _tcpClient = new();
|
private TcpClientWrapper _tcpClient = null;
|
||||||
private readonly Timer _idleTimer;
|
private readonly Timer _idleTimer;
|
||||||
|
|
||||||
private readonly Task _processingTask;
|
private readonly Task _processingTask;
|
||||||
private readonly AsyncQueue<RequestQueueItem> _requestQueue = new();
|
private readonly AsyncQueue<RequestQueueItem> _requestQueue = new();
|
||||||
|
|
||||||
|
private TimeSpan _readTimeout = TimeSpan.FromSeconds(1);
|
||||||
|
private TimeSpan _writeTimeout = TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
#endregion Fields
|
#endregion Fields
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -58,15 +62,33 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual TimeSpan ReadTimeout
|
public virtual TimeSpan ReadTimeout
|
||||||
{
|
{
|
||||||
get => TimeSpan.FromMilliseconds(_tcpClient.ReceiveTimeout);
|
get => _readTimeout;
|
||||||
set => _tcpClient.ReceiveTimeout = (int)value.TotalMilliseconds;
|
set
|
||||||
|
{
|
||||||
|
if (value < TimeSpan.Zero)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(value));
|
||||||
|
|
||||||
|
_readTimeout = value;
|
||||||
|
|
||||||
|
if (_tcpClient != null)
|
||||||
|
_tcpClient.ReceiveTimeout = (int)value.TotalMilliseconds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual TimeSpan WriteTimeout
|
public virtual TimeSpan WriteTimeout
|
||||||
{
|
{
|
||||||
get => TimeSpan.FromMilliseconds(_tcpClient.SendTimeout);
|
get => _writeTimeout;
|
||||||
set => _tcpClient.SendTimeout = (int)value.TotalMilliseconds;
|
set
|
||||||
|
{
|
||||||
|
if (value < TimeSpan.Zero)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(value));
|
||||||
|
|
||||||
|
_writeTimeout = value;
|
||||||
|
|
||||||
|
if (_tcpClient != null)
|
||||||
|
_tcpClient.SendTimeout = (int)value.TotalMilliseconds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -116,7 +138,6 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_processingTask.Wait();
|
|
||||||
_processingTask.Dispose();
|
_processingTask.Dispose();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -124,7 +145,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
|
|
||||||
OnIdleTimer(null);
|
OnIdleTimer(null);
|
||||||
|
|
||||||
_tcpClient.Dispose();
|
_tcpClient?.Dispose();
|
||||||
_clientLock.Dispose();
|
_clientLock.Dispose();
|
||||||
|
|
||||||
while (_requestQueue.TryDequeue(out var item))
|
while (_requestQueue.TryDequeue(out var item))
|
||||||
@@ -164,7 +185,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
{
|
{
|
||||||
Request = [.. request],
|
Request = [.. request],
|
||||||
ValidateResponseComplete = validateResponseComplete,
|
ValidateResponseComplete = validateResponseComplete,
|
||||||
TaskCompletionSource = new(),
|
TaskCompletionSource = new TaskCompletionSource<IReadOnlyList<byte>>(),
|
||||||
CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)
|
CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -187,7 +208,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Get next request to process
|
// Get next request to process
|
||||||
var item = await _requestQueue.DequeueAsync(cancellationToken).ConfigureAwait(false);
|
var item = await _requestQueue.DequeueAsync(cancellationToken);
|
||||||
|
|
||||||
// Remove registration => already removed from queue
|
// Remove registration => already removed from queue
|
||||||
item.CancellationTokenRegistration.Dispose();
|
item.CancellationTokenRegistration.Dispose();
|
||||||
@@ -195,19 +216,19 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
// Build combined cancellation token
|
// Build combined cancellation token
|
||||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
|
||||||
// Wait for exclusive access
|
// Wait for exclusive access
|
||||||
await _clientLock.WaitAsync(linkedCts.Token).ConfigureAwait(false);
|
await _clientLock.WaitAsync(linkedCts.Token);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Ensure connection is up
|
// Ensure connection is up
|
||||||
await AssertConnection(linkedCts.Token).ConfigureAwait(false);
|
await AssertConnection(linkedCts.Token);
|
||||||
|
|
||||||
var stream = _tcpClient.GetStream();
|
var stream = _tcpClient.GetStream();
|
||||||
await stream.FlushAsync(linkedCts.Token).ConfigureAwait(false);
|
await stream.FlushAsync(linkedCts.Token);
|
||||||
|
|
||||||
#if NET6_0_OR_GREATER
|
#if NET6_0_OR_GREATER
|
||||||
await stream.WriteAsync(item.Request, linkedCts.Token).ConfigureAwait(false);
|
await stream.WriteAsync(item.Request, linkedCts.Token);
|
||||||
#else
|
#else
|
||||||
await stream.WriteAsync(item.Request, 0, item.Request.Length, linkedCts.Token).ConfigureAwait(false);
|
await stream.WriteAsync(item.Request, 0, item.Request.Length, linkedCts.Token);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
linkedCts.Token.ThrowIfCancellationRequested();
|
linkedCts.Token.ThrowIfCancellationRequested();
|
||||||
@@ -218,9 +239,9 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
do
|
do
|
||||||
{
|
{
|
||||||
#if NET6_0_OR_GREATER
|
#if NET6_0_OR_GREATER
|
||||||
int readCount = await stream.ReadAsync(buffer, linkedCts.Token).ConfigureAwait(false);
|
int readCount = await stream.ReadAsync(buffer, linkedCts.Token);
|
||||||
#else
|
#else
|
||||||
int readCount = await stream.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token).ConfigureAwait(false);
|
int readCount = await stream.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token);
|
||||||
#endif
|
#endif
|
||||||
if (readCount < 1)
|
if (readCount < 1)
|
||||||
throw new EndOfStreamException();
|
throw new EndOfStreamException();
|
||||||
@@ -267,7 +288,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
// Has to be called within _clientLock!
|
// Has to be called within _clientLock!
|
||||||
private async Task AssertConnection(CancellationToken cancellationToken)
|
private async Task AssertConnection(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (_tcpClient.Connected)
|
if (_tcpClient?.Connected == true)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
int delay = 1;
|
int delay = 1;
|
||||||
@@ -284,14 +305,16 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
{
|
{
|
||||||
foreach (var ipAddress in ipAddresses)
|
foreach (var ipAddress in ipAddresses)
|
||||||
{
|
{
|
||||||
_tcpClient.Close();
|
_tcpClient?.Close();
|
||||||
|
_tcpClient?.Dispose();
|
||||||
|
|
||||||
|
_tcpClient = _tcpClientFactory.Create(ipAddress.AddressFamily, _readTimeout, _writeTimeout);
|
||||||
#if NET6_0_OR_GREATER
|
#if NET6_0_OR_GREATER
|
||||||
using var connectTask = _tcpClient.ConnectAsync(ipAddress, Port, cancellationToken);
|
var connectTask = _tcpClient.ConnectAsync(ipAddress, Port, cancellationToken);
|
||||||
#else
|
#else
|
||||||
using var connectTask = _tcpClient.ConnectAsync(ipAddress, Port);
|
var connectTask = _tcpClient.ConnectAsync(ipAddress, Port);
|
||||||
#endif
|
#endif
|
||||||
if (await Task.WhenAny(connectTask, Task.Delay(ReadTimeout, cancellationToken)) == connectTask)
|
if (await Task.WhenAny(connectTask, Task.Delay(_readTimeout, cancellationToken)) == connectTask)
|
||||||
{
|
{
|
||||||
await connectTask;
|
await connectTask;
|
||||||
if (_tcpClient.Connected)
|
if (_tcpClient.Connected)
|
||||||
@@ -309,7 +332,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(false);
|
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{ /* keep it quiet */ }
|
{ /* keep it quiet */ }
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
private readonly ReaderWriterLockSlim _deviceListLock = new();
|
private readonly ReaderWriterLockSlim _deviceListLock = new();
|
||||||
private readonly Dictionary<byte, ModbusDevice> _devices = [];
|
private readonly Dictionary<byte, ModbusDevice> _devices = [];
|
||||||
|
|
||||||
|
private TimeSpan _readWriteTimeout = TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
#endregion Fields
|
#endregion Fields
|
||||||
|
|
||||||
#region Constructors
|
#region Constructors
|
||||||
@@ -49,7 +51,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
{
|
{
|
||||||
ListenAddress = listenAddress ?? IPAddress.Loopback;
|
ListenAddress = listenAddress ?? IPAddress.Loopback;
|
||||||
|
|
||||||
if (ushort.MinValue < listenPort || listenPort < ushort.MaxValue)
|
if (listenPort < ushort.MinValue || ushort.MaxValue < listenPort)
|
||||||
throw new ArgumentOutOfRangeException(nameof(listenPort));
|
throw new ArgumentOutOfRangeException(nameof(listenPort));
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -105,7 +107,17 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the read/write timeout.
|
/// Gets or sets the read/write timeout.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan ReadWriteTimeout { get; set; }
|
public TimeSpan ReadWriteTimeout
|
||||||
|
{
|
||||||
|
get => _readWriteTimeout;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value < TimeSpan.Zero)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(value));
|
||||||
|
|
||||||
|
_readWriteTimeout = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endregion Properties
|
#endregion Properties
|
||||||
|
|
||||||
@@ -151,11 +163,11 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
|
|
||||||
private async Task StopAsyncInternal(CancellationToken cancellationToken = default)
|
private async Task StopAsyncInternal(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
_stopCts.Cancel();
|
_stopCts?.Cancel();
|
||||||
|
|
||||||
_listener.Stop();
|
_listener?.Stop();
|
||||||
#if NET8_0_OR_GREATER
|
#if NET8_0_OR_GREATER
|
||||||
_listener.Dispose();
|
_listener?.Dispose();
|
||||||
#endif
|
#endif
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -193,6 +205,8 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
|
|
||||||
_clients.Clear();
|
_clients.Clear();
|
||||||
_devices.Clear();
|
_devices.Clear();
|
||||||
|
|
||||||
|
_stopCts?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Assertions()
|
private void Assertions()
|
||||||
@@ -216,11 +230,11 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
#if NET8_0_OR_GREATER
|
#if NET8_0_OR_GREATER
|
||||||
var client = await _listener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(false);
|
var client = await _listener.AcceptTcpClientAsync(cancellationToken);
|
||||||
#else
|
#else
|
||||||
var client = await _listener.AcceptTcpClientAsync().ConfigureAwait(false);
|
var client = await _listener.AcceptTcpClientAsync();
|
||||||
#endif
|
#endif
|
||||||
await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
await _clientListLock.WaitAsync(cancellationToken);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_clients.Add(client);
|
_clients.Add(client);
|
||||||
@@ -250,20 +264,20 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
using (var cts = new CancellationTokenSource(ReadWriteTimeout))
|
using (var cts = new CancellationTokenSource(ReadWriteTimeout))
|
||||||
using (cancellationToken.Register(cts.Cancel))
|
using (cancellationToken.Register(cts.Cancel))
|
||||||
{
|
{
|
||||||
byte[] headerBytes = await stream.ReadExpectedBytesAsync(6, cts.Token).ConfigureAwait(false);
|
byte[] headerBytes = await stream.ReadExpectedBytesAsync(6, cts.Token);
|
||||||
requestBytes.AddRange(headerBytes);
|
requestBytes.AddRange(headerBytes);
|
||||||
|
|
||||||
byte[] followingCountBytes = headerBytes.Skip(4).Take(2).ToArray();
|
byte[] followingCountBytes = headerBytes.Skip(4).Take(2).ToArray();
|
||||||
followingCountBytes.SwapBigEndian();
|
followingCountBytes.SwapBigEndian();
|
||||||
int followingCount = BitConverter.ToUInt16(followingCountBytes, 0);
|
int followingCount = BitConverter.ToUInt16(followingCountBytes, 0);
|
||||||
|
|
||||||
byte[] bodyBytes = await stream.ReadExpectedBytesAsync(followingCount, cts.Token).ConfigureAwait(false);
|
byte[] bodyBytes = await stream.ReadExpectedBytesAsync(followingCount, cts.Token);
|
||||||
requestBytes.AddRange(bodyBytes);
|
requestBytes.AddRange(bodyBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] responseBytes = HandleRequest([.. requestBytes]);
|
byte[] responseBytes = HandleRequest([.. requestBytes]);
|
||||||
if (responseBytes != null)
|
if (responseBytes != null)
|
||||||
await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken).ConfigureAwait(false);
|
await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -272,7 +286,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
await _clientListLock.WaitAsync(cancellationToken);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_clients.Remove(client);
|
_clients.Remove(client);
|
||||||
@@ -352,7 +366,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
||||||
ushort count = requestBytes.GetBigEndianUInt16(10);
|
ushort count = requestBytes.GetBigEndianUInt16(10);
|
||||||
|
|
||||||
if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_DISCRETE_READ_COUNT)
|
if (count < TcpProtocol.MIN_READ_COUNT || TcpProtocol.MAX_DISCRETE_READ_COUNT < count)
|
||||||
{
|
{
|
||||||
responseBytes[7] |= 0x80;
|
responseBytes[7] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
@@ -404,7 +418,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
||||||
ushort count = requestBytes.GetBigEndianUInt16(10);
|
ushort count = requestBytes.GetBigEndianUInt16(10);
|
||||||
|
|
||||||
if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_DISCRETE_READ_COUNT)
|
if (count < TcpProtocol.MIN_READ_COUNT || TcpProtocol.MAX_DISCRETE_READ_COUNT < count)
|
||||||
{
|
{
|
||||||
responseBytes[7] |= 0x80;
|
responseBytes[7] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
@@ -456,7 +470,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
||||||
ushort count = requestBytes.GetBigEndianUInt16(10);
|
ushort count = requestBytes.GetBigEndianUInt16(10);
|
||||||
|
|
||||||
if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_REGISTER_READ_COUNT)
|
if (count < TcpProtocol.MIN_READ_COUNT || TcpProtocol.MAX_REGISTER_READ_COUNT < count)
|
||||||
{
|
{
|
||||||
responseBytes[7] |= 0x80;
|
responseBytes[7] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
@@ -505,7 +519,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
||||||
ushort count = requestBytes.GetBigEndianUInt16(10);
|
ushort count = requestBytes.GetBigEndianUInt16(10);
|
||||||
|
|
||||||
if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_REGISTER_READ_COUNT)
|
if (count < TcpProtocol.MIN_READ_COUNT || TcpProtocol.MAX_REGISTER_READ_COUNT < count)
|
||||||
{
|
{
|
||||||
responseBytes[7] |= 0x80;
|
responseBytes[7] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
|||||||
@@ -23,6 +23,18 @@ float voltage = registers.GetSingle();
|
|||||||
Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V");
|
Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V");
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you have a device speaking `RTU` connected over `TCP`, you can use it as followed:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// [...]
|
||||||
|
|
||||||
|
using var client = new ModbusTcpClient(host, port)
|
||||||
|
{
|
||||||
|
Protocol = new RtuProtocol()
|
||||||
|
};
|
||||||
|
|
||||||
|
// [...]
|
||||||
|
```
|
||||||
|
|
||||||
## Sources
|
## Sources
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ namespace AMWD.Protocols.Modbus.Tcp.Utils
|
|||||||
{
|
{
|
||||||
/// <inheritdoc cref="TcpClient" />
|
/// <inheritdoc cref="TcpClient" />
|
||||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
internal class TcpClientWrapper : IDisposable
|
internal class TcpClientWrapper(AddressFamily addressFamily) : IDisposable
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
|
|
||||||
private readonly TcpClient _client = new();
|
private readonly TcpClient _client = new(addressFamily);
|
||||||
|
|
||||||
#endregion Fields
|
#endregion Fields
|
||||||
|
|
||||||
|
|||||||
26
AMWD.Protocols.Modbus.Tcp/Utils/TcpClientWrapperFactory.cs
Normal file
26
AMWD.Protocols.Modbus.Tcp/Utils/TcpClientWrapperFactory.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
|
||||||
|
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||||
|
{
|
||||||
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
|
internal class TcpClientWrapperFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of <see cref="TcpClientWrapper"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="addressFamily">The <see cref="AddressFamily"/> of the <see cref="TcpClient"/> to use.</param>
|
||||||
|
/// <param name="readTimeout">The read timeout.</param>
|
||||||
|
/// <param name="writeTimeout">The write timeout.</param>
|
||||||
|
/// <returns>A new <see cref="TcpClientWrapper"/> instance.</returns>
|
||||||
|
public virtual TcpClientWrapper Create(AddressFamily addressFamily, TimeSpan readTimeout, TimeSpan writeTimeout)
|
||||||
|
{
|
||||||
|
var client = new TcpClientWrapper(addressFamily)
|
||||||
|
{
|
||||||
|
ReceiveTimeout = (int)readTimeout.TotalMilliseconds,
|
||||||
|
SendTimeout = (int)writeTimeout.TotalMilliseconds
|
||||||
|
};
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,14 +13,18 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.msbuild" Version="6.0.1">
|
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||||
<PackageReference Include="Moq" Version="4.20.70" />
|
<PackageReference Include="Moq" Version="4.20.70" />
|
||||||
<PackageReference Include="MSTest.TestAdapter" Version="3.2.2" />
|
<PackageReference Include="MSTest.TestAdapter" Version="3.4.3" />
|
||||||
<PackageReference Include="MSTest.TestFramework" Version="3.2.2" />
|
<PackageReference Include="MSTest.TestFramework" Version="3.4.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -16,6 +17,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
private readonly string _hostname = "127.0.0.1";
|
private readonly string _hostname = "127.0.0.1";
|
||||||
|
|
||||||
private Mock<TcpClientWrapper> _tcpClientMock;
|
private Mock<TcpClientWrapper> _tcpClientMock;
|
||||||
|
private Mock<TcpClientWrapperFactory> _tcpClientFactoryMock;
|
||||||
private Mock<NetworkStreamWrapper> _networkStreamMock;
|
private Mock<NetworkStreamWrapper> _networkStreamMock;
|
||||||
|
|
||||||
private bool _alwaysConnected;
|
private bool _alwaysConnected;
|
||||||
@@ -40,10 +42,19 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void ShouldGetAndSetPropertiesOfBaseClient()
|
public async Task ShouldSetPropertiesOfBaseClient()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
byte[] request = [1, 2, 3];
|
||||||
|
byte[] expectedResponse = [9, 8, 7];
|
||||||
|
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||||
|
_networkResponseQueue.Enqueue(expectedResponse);
|
||||||
|
|
||||||
var connection = GetTcpConnection();
|
var connection = GetTcpConnection();
|
||||||
|
await connection.InvokeAsync(request, validation);
|
||||||
|
|
||||||
|
_tcpClientMock.Invocations.Clear();
|
||||||
|
_networkStreamMock.Invocations.Clear();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
connection.ReadTimeout = TimeSpan.FromSeconds(123);
|
connection.ReadTimeout = TimeSpan.FromSeconds(123);
|
||||||
@@ -51,8 +62,8 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
|
|
||||||
// Assert - part 1
|
// Assert - part 1
|
||||||
Assert.AreEqual("TCP", connection.Name);
|
Assert.AreEqual("TCP", connection.Name);
|
||||||
Assert.AreEqual(1, connection.ReadTimeout.TotalSeconds);
|
Assert.AreEqual(123, connection.ReadTimeout.TotalSeconds);
|
||||||
Assert.AreEqual(1, connection.WriteTimeout.TotalSeconds);
|
Assert.AreEqual(456, connection.WriteTimeout.TotalSeconds);
|
||||||
|
|
||||||
Assert.AreEqual(_hostname, connection.Hostname);
|
Assert.AreEqual(_hostname, connection.Hostname);
|
||||||
Assert.AreEqual(502, connection.Port);
|
Assert.AreEqual(502, connection.Port);
|
||||||
@@ -61,9 +72,6 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
_tcpClientMock.VerifySet(c => c.ReceiveTimeout = 123000, Times.Once);
|
_tcpClientMock.VerifySet(c => c.ReceiveTimeout = 123000, Times.Once);
|
||||||
_tcpClientMock.VerifySet(c => c.SendTimeout = 456000, Times.Once);
|
_tcpClientMock.VerifySet(c => c.SendTimeout = 456000, Times.Once);
|
||||||
|
|
||||||
_tcpClientMock.VerifyGet(c => c.ReceiveTimeout, Times.Once);
|
|
||||||
_tcpClientMock.VerifyGet(c => c.SendTimeout, Times.Once);
|
|
||||||
|
|
||||||
_tcpClientMock.VerifyNoOtherCalls();
|
_tcpClientMock.VerifyNoOtherCalls();
|
||||||
_networkStreamMock.VerifyNoOtherCalls();
|
_networkStreamMock.VerifyNoOtherCalls();
|
||||||
}
|
}
|
||||||
@@ -173,6 +181,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
||||||
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
|
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
|
||||||
|
|
||||||
|
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
_tcpClientMock.Verify(c => c.Connected, Times.Once);
|
_tcpClientMock.Verify(c => c.Connected, Times.Once);
|
||||||
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
||||||
|
|
||||||
@@ -211,11 +220,10 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
||||||
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
|
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
|
||||||
|
|
||||||
_tcpClientMock.VerifyGet(c => c.ReceiveTimeout, Times.Once);
|
|
||||||
|
|
||||||
_tcpClientMock.Verify(c => c.Connected, Times.Exactly(3));
|
_tcpClientMock.Verify(c => c.Connected, Times.Exactly(3));
|
||||||
_tcpClientMock.Verify(c => c.Close(), Times.Exactly(2));
|
_tcpClientMock.Verify(c => c.Close(), Times.Exactly(2));
|
||||||
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
_tcpClientMock.Verify(c => c.Dispose(), Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
|
||||||
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
||||||
|
|
||||||
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||||
@@ -289,11 +297,10 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
||||||
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
|
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
|
||||||
|
|
||||||
_tcpClientMock.VerifyGet(c => c.ReceiveTimeout, Times.Once);
|
|
||||||
|
|
||||||
_tcpClientMock.Verify(c => c.Connected, Times.Exactly(3));
|
_tcpClientMock.Verify(c => c.Connected, Times.Exactly(3));
|
||||||
_tcpClientMock.Verify(c => c.Close(), Times.Once);
|
_tcpClientMock.Verify(c => c.Close(), Times.Once);
|
||||||
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
_tcpClientMock.Verify(c => c.Dispose(), Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
|
||||||
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
||||||
|
|
||||||
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||||
@@ -329,11 +336,10 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
||||||
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
|
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
|
||||||
|
|
||||||
_tcpClientMock.VerifyGet(c => c.ReceiveTimeout, Times.Exactly(2));
|
|
||||||
|
|
||||||
_tcpClientMock.Verify(c => c.Connected, Times.Exactly(3));
|
_tcpClientMock.Verify(c => c.Connected, Times.Exactly(3));
|
||||||
_tcpClientMock.Verify(c => c.Close(), Times.Exactly(2));
|
_tcpClientMock.Verify(c => c.Close(), Times.Exactly(2));
|
||||||
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
|
_tcpClientMock.Verify(c => c.Dispose(), Times.Exactly(2));
|
||||||
|
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Exactly(3));
|
||||||
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
||||||
|
|
||||||
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||||
@@ -426,6 +432,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
||||||
|
|
||||||
_tcpClientMock.Verify(c => c.Connected, Times.Once);
|
_tcpClientMock.Verify(c => c.Connected, Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
||||||
|
|
||||||
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||||
@@ -475,6 +482,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
|
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
|
||||||
|
|
||||||
_tcpClientMock.Verify(c => c.Connected, Times.Once);
|
_tcpClientMock.Verify(c => c.Connected, Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
||||||
_tcpClientMock.Verify(c => c.Dispose(), Times.Once);
|
_tcpClientMock.Verify(c => c.Dispose(), Times.Once);
|
||||||
|
|
||||||
@@ -508,7 +516,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
return ValueTask.FromResult(0);
|
return ValueTask.FromResult(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
_tcpClientMock = new Mock<TcpClientWrapper>();
|
_tcpClientMock = new Mock<TcpClientWrapper>(AddressFamily.Unknown);
|
||||||
_tcpClientMock.Setup(c => c.Connected).Returns(() => _alwaysConnected || _connectedQueue.Dequeue());
|
_tcpClientMock.Setup(c => c.Connected).Returns(() => _alwaysConnected || _connectedQueue.Dequeue());
|
||||||
_tcpClientMock.Setup(c => c.ReceiveTimeout).Returns(() => _clientReceiveTimeout);
|
_tcpClientMock.Setup(c => c.ReceiveTimeout).Returns(() => _clientReceiveTimeout);
|
||||||
_tcpClientMock.Setup(c => c.SendTimeout).Returns(() => _clientSendTimeout);
|
_tcpClientMock.Setup(c => c.SendTimeout).Returns(() => _clientSendTimeout);
|
||||||
@@ -521,6 +529,11 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
.Setup(c => c.GetStream())
|
.Setup(c => c.GetStream())
|
||||||
.Returns(() => _networkStreamMock.Object);
|
.Returns(() => _networkStreamMock.Object);
|
||||||
|
|
||||||
|
_tcpClientFactoryMock = new Mock<TcpClientWrapperFactory>();
|
||||||
|
_tcpClientFactoryMock
|
||||||
|
.Setup(c => c.Create(It.IsAny<AddressFamily>(), It.IsAny<TimeSpan>(), It.IsAny<TimeSpan>()))
|
||||||
|
.Returns(_tcpClientMock.Object);
|
||||||
|
|
||||||
var connection = new ModbusTcpConnection
|
var connection = new ModbusTcpConnection
|
||||||
{
|
{
|
||||||
Hostname = _hostname,
|
Hostname = _hostname,
|
||||||
@@ -528,9 +541,9 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Replace real connection with mock
|
// Replace real connection with mock
|
||||||
var connectionField = connection.GetType().GetField("_tcpClient", BindingFlags.NonPublic | BindingFlags.Instance);
|
var factoryField = connection.GetType().GetField("_tcpClientFactory", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
(connectionField.GetValue(connection) as TcpClientWrapper)?.Dispose();
|
(factoryField.GetValue(connection) as TcpClientWrapper)?.Dispose();
|
||||||
connectionField.SetValue(connection, _tcpClientMock.Object);
|
factoryField.SetValue(connection, _tcpClientFactoryMock.Object);
|
||||||
|
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,11 +29,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{C8065AE3
|
|||||||
Directory.Build.props = Directory.Build.props
|
Directory.Build.props = Directory.Build.props
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMWD.Protocols.Modbus.Tests", "AMWD.Protocols.Modbus.Tests\AMWD.Protocols.Modbus.Tests.csproj", "{146070C4-E922-4F5A-AD6F-9A899186E26E}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Tests", "AMWD.Protocols.Modbus.Tests\AMWD.Protocols.Modbus.Tests.csproj", "{146070C4-E922-4F5A-AD6F-9A899186E26E}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMWD.Protocols.Modbus.Tcp", "AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj", "{8C888A84-CD09-4087-B5DA-67708ABBABA2}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Tcp", "AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj", "{8C888A84-CD09-4087-B5DA-67708ABBABA2}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMWD.Protocols.Modbus.Serial", "AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj", "{D966826F-EE6C-4BC0-9185-C2A9A50FD586}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Serial", "AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj", "{D966826F-EE6C-4BC0-9185-C2A9A50FD586}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Proxy", "AMWD.Protocols.Modbus.Proxy\AMWD.Protocols.Modbus.Proxy.csproj", "{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -57,6 +59,10 @@ Global
|
|||||||
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Release|Any CPU.Build.0 = Release|Any CPU
|
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
BIN
AMWD.Protocols.Modbus.snk
Normal file
BIN
AMWD.Protocols.Modbus.snk
Normal file
Binary file not shown.
39
CHANGELOG.md
39
CHANGELOG.md
@@ -7,12 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
_nothing changed yet_
|
_no changes_
|
||||||
|
|
||||||
|
|
||||||
|
## [v0.3.2] (2024-09-04)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Build configuration for strong named assemblies.
|
||||||
|
|
||||||
|
|
||||||
|
## [v0.3.1] (2024-06-28)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Issues with range validation on several lines of code in server implementations.
|
||||||
|
|
||||||
|
|
||||||
|
## [v0.3.0] (2024-05-31)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- New `AMWD.Protocols.Modbus.Proxy` package, that contains the server implementations as proxies
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Renamed `ModbusSerialServer` to `ModbusRtuServer` to clearify the protocol that is used
|
||||||
|
- Made `Protocol` property of `ModbusClientBase` non-abstract
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Issue with missing client on TCP connection when using default constructor (seems that `AddressFamily.Unknown` caused the problem)
|
||||||
|
|
||||||
|
|
||||||
## [v0.2.0] (2024-04-02)
|
## [v0.2.0] (2024-04-02)
|
||||||
|
|
||||||
First "final" re-implementation.
|
First "final" re-implementation
|
||||||
|
|
||||||
|
|
||||||
## v0.1.0 (2022-08-28)
|
## v0.1.0 (2022-08-28)
|
||||||
@@ -22,5 +52,8 @@ So this tag is only here for documentation purposes of the NuGet Gallery.
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.2.0...HEAD
|
[Unreleased]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.2...HEAD
|
||||||
|
[v0.3.2]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.1...v0.3.2
|
||||||
|
[v0.3.1]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.0...v0.3.1
|
||||||
|
[v0.3.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.2.0...v0.3.0
|
||||||
[v0.2.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/tree/v0.2.0
|
[v0.2.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/tree/v0.2.0
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
<Authors>Andreas Müller</Authors>
|
<Authors>Andreas Müller</Authors>
|
||||||
<Copyright>© {copyright:2018-} AM.WD</Copyright>
|
<Copyright>© {copyright:2018-} AM.WD</Copyright>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
|
||||||
|
<SignAssembly>true</SignAssembly>
|
||||||
|
<AssemblyOriginatorKeyFile>../AMWD.Protocols.Modbus.snk</AssemblyOriginatorKeyFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(GITLAB_CI)' == 'true'">
|
<PropertyGroup Condition="'$(GITLAB_CI)' == 'true'">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Here you can find a basic implementation of the Modbus protocol.
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The project is divided into three parts.
|
The project is divided into four parts.
|
||||||
|
|
||||||
To be mentioned at the beginning:
|
To be mentioned at the beginning:
|
||||||
Only the clients are build very modular to fit any requirement reached on the first implementation back in 2018 ([see here]).
|
Only the clients are build very modular to fit any requirement reached on the first implementation back in 2018 ([see here]).
|
||||||
@@ -20,6 +20,11 @@ For example the default protocol versions: `TCP`, `RTU` and `ASCII`.
|
|||||||
With this package you'll have anything you need to create your own client implementations.
|
With this package you'll have anything you need to create your own client implementations.
|
||||||
|
|
||||||
|
|
||||||
|
### [Proxy]
|
||||||
|
|
||||||
|
The package contains a TCP and a RTU server implementation as proxy which contains a client of your choice to connect to.
|
||||||
|
|
||||||
|
|
||||||
### [Serial]
|
### [Serial]
|
||||||
|
|
||||||
This package contains some wrappers and implementations for the serial protocol.
|
This package contains some wrappers and implementations for the serial protocol.
|
||||||
@@ -42,6 +47,7 @@ Published under [MIT License] (see [**tl;dr**Legal])
|
|||||||
|
|
||||||
[see here]: https://github.com/andreasAMmueller/Modbus
|
[see here]: https://github.com/andreasAMmueller/Modbus
|
||||||
[Common]: AMWD.Protocols.Modbus.Common/README.md
|
[Common]: AMWD.Protocols.Modbus.Common/README.md
|
||||||
|
[Proxy]: AMWD.Protocols.Modbus.Proxy/README.md
|
||||||
[Serial]: AMWD.Protocols.Modbus.Serial/README.md
|
[Serial]: AMWD.Protocols.Modbus.Serial/README.md
|
||||||
[TCP]: AMWD.Protocols.Modbus.Tcp/README.md
|
[TCP]: AMWD.Protocols.Modbus.Tcp/README.md
|
||||||
[MIT License]: LICENSE.txt
|
[MIT License]: LICENSE.txt
|
||||||
|
|||||||
Reference in New Issue
Block a user