Async optimization
This commit is contained in:
@@ -10,19 +10,27 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
/// <summary>
|
||||
/// Base implementation of a Modbus client.
|
||||
/// </summary>
|
||||
public abstract class ModbusClientBase : IDisposable
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="ModbusClientBase"/> class with a specific <see cref="IModbusConnection"/>.
|
||||
/// </remarks>
|
||||
/// <param name="connection">The <see cref="IModbusConnection"/> responsible for invoking the requests.</param>
|
||||
/// <param name="disposeConnection">
|
||||
/// <see langword="true"/> if the connection should be disposed of by Dispose(),
|
||||
/// <see langword="false"/> otherwise if you inted to reuse the connection.
|
||||
/// </param>
|
||||
public abstract class ModbusClientBase(IModbusConnection connection, bool disposeConnection) : IDisposable
|
||||
{
|
||||
private bool _isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the connection should be disposed of by <see cref="Dispose()"/>.
|
||||
/// </summary>
|
||||
protected readonly bool disposeConnection;
|
||||
protected readonly bool disposeConnection = disposeConnection;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="IModbusConnection"/> responsible for invoking the requests.
|
||||
/// </summary>
|
||||
protected readonly IModbusConnection connection;
|
||||
protected readonly IModbusConnection connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModbusClientBase"/> class with a specific <see cref="IModbusConnection"/>.
|
||||
@@ -32,20 +40,6 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
: this(connection, true)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModbusClientBase"/> class with a specific <see cref="IModbusConnection"/>.
|
||||
/// </summary>
|
||||
/// <param name="connection">The <see cref="IModbusConnection"/> responsible for invoking the requests.</param>
|
||||
/// <param name="disposeConnection">
|
||||
/// <see langword="true"/> if the connection should be disposed of by Dispose(),
|
||||
/// <see langword="false"/> otherwise if you inted to reuse the connection.
|
||||
/// </param>
|
||||
public ModbusClientBase(IModbusConnection connection, bool disposeConnection)
|
||||
{
|
||||
this.connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||
this.disposeConnection = disposeConnection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the protocol type to use.
|
||||
/// </summary>
|
||||
@@ -67,7 +61,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeReadCoils(unitId, startAddress, count);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
// The protocol processes complete bytes from the response.
|
||||
@@ -92,7 +86,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeReadDiscreteInputs(unitId, startAddress, count);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
// The protocol processes complete bytes from the response.
|
||||
@@ -117,7 +111,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeReadHoldingRegisters(unitId, startAddress, count);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
var holdingRegisters = Protocol.DeserializeReadHoldingRegisters(response).ToList();
|
||||
@@ -140,7 +134,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeReadInputRegisters(unitId, startAddress, count);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
var inputRegisters = Protocol.DeserializeReadInputRegisters(response).ToList();
|
||||
@@ -184,7 +178,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
do
|
||||
{
|
||||
var request = Protocol.SerializeReadDeviceIdentification(unitId, category, requestObjectId);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
result = Protocol.DeserializeReadDeviceIdentification(response);
|
||||
@@ -247,7 +241,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeWriteSingleCoil(unitId, coil);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
var result = Protocol.DeserializeWriteSingleCoil(response);
|
||||
@@ -268,7 +262,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeWriteSingleHoldingRegister(unitId, register);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
var result = Protocol.DeserializeWriteSingleHoldingRegister(response);
|
||||
@@ -289,7 +283,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeWriteMultipleCoils(unitId, coils);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
var (firstAddress, count) = Protocol.DeserializeWriteMultipleCoils(response);
|
||||
@@ -309,7 +303,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeWriteMultipleHoldingRegisters(unitId, registers);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
var (firstAddress, count) = Protocol.DeserializeWriteMultipleHoldingRegisters(response);
|
||||
|
||||
@@ -192,17 +192,16 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
public Task StopAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Assertions();
|
||||
return StopAsyncInternal(cancellationToken);
|
||||
StopAsyncInternal();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task StopAsyncInternal(CancellationToken cancellationToken)
|
||||
private void StopAsyncInternal()
|
||||
{
|
||||
_stopCts?.Cancel();
|
||||
|
||||
_serialPort.Close();
|
||||
_serialPort.DataReceived -= OnDataReceived;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -215,7 +214,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
|
||||
_isDisposed = true;
|
||||
|
||||
StopAsyncInternal(CancellationToken.None).Wait();
|
||||
StopAsyncInternal();
|
||||
|
||||
_serialPort.Dispose();
|
||||
_stopCts?.Dispose();
|
||||
@@ -332,7 +331,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
responseBytes.AddRange(requestBytes.Take(2));
|
||||
try
|
||||
{
|
||||
var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken);
|
||||
var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)];
|
||||
for (int i = 0; i < coils.Count; i++)
|
||||
@@ -371,7 +370,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
responseBytes.AddRange(requestBytes.Take(2));
|
||||
try
|
||||
{
|
||||
var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken);
|
||||
var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)];
|
||||
for (int i = 0; i < discreteInputs.Count; i++)
|
||||
@@ -410,7 +409,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
responseBytes.AddRange(requestBytes.Take(2));
|
||||
try
|
||||
{
|
||||
var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken);
|
||||
var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
byte[] values = new byte[holdingRegisters.Count * 2];
|
||||
for (int i = 0; i < holdingRegisters.Count; i++)
|
||||
@@ -444,7 +443,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
responseBytes.AddRange(requestBytes.Take(2));
|
||||
try
|
||||
{
|
||||
var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken);
|
||||
var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
byte[] values = new byte[count * 2];
|
||||
for (int i = 0; i < count; i++)
|
||||
@@ -492,7 +491,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
LowByte = requestBytes[5],
|
||||
};
|
||||
|
||||
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[0], coil, cancellationToken);
|
||||
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[0], coil, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (isSuccess)
|
||||
{
|
||||
// Response is an echo of the request
|
||||
@@ -531,7 +530,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
LowByte = requestBytes[5]
|
||||
};
|
||||
|
||||
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[0], register, cancellationToken);
|
||||
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[0], register, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (isSuccess)
|
||||
{
|
||||
// Response is an echo of the request
|
||||
@@ -591,7 +590,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
});
|
||||
}
|
||||
|
||||
bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[0], coils, cancellationToken);
|
||||
bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[0], coils, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (isSuccess)
|
||||
{
|
||||
// Response is an echo of the request
|
||||
@@ -648,7 +647,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
});
|
||||
}
|
||||
|
||||
bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[0], list, cancellationToken);
|
||||
bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[0], list, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (isSuccess)
|
||||
{
|
||||
// Response is an echo of the request
|
||||
@@ -705,7 +704,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
|
||||
try
|
||||
{
|
||||
var deviceInfo = await Client.ReadDeviceIdentificationAsync(requestBytes[0], category, firstObject, cancellationToken);
|
||||
var deviceInfo = await Client.ReadDeviceIdentificationAsync(requestBytes[0], category, firstObject, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
var bodyBytes = new List<byte>();
|
||||
|
||||
@@ -855,5 +854,21 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
}
|
||||
|
||||
#endregion Request Handling
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine($"RTU Proxy");
|
||||
sb.AppendLine($" {nameof(PortName)}: {PortName}");
|
||||
sb.AppendLine($" {nameof(BaudRate)}: {(int)BaudRate}");
|
||||
sb.AppendLine($" {nameof(DataBits)}: {DataBits}");
|
||||
sb.AppendLine($" {nameof(StopBits)}: {StopBits}");
|
||||
sb.AppendLine($" {nameof(Parity)}: {Parity}");
|
||||
sb.AppendLine($" {nameof(Client)}: {Client.GetType().Name}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
private readonly Task _processingTask;
|
||||
private readonly AsyncQueue<RequestQueueItem> _requestQueue = new();
|
||||
|
||||
// Only required to cover all logic branches on unit tests.
|
||||
private bool _isUnitTest = false;
|
||||
private readonly bool _isLinux;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
@@ -41,6 +40,8 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
/// </summary>
|
||||
public ModbusSerialConnection(string portName)
|
||||
{
|
||||
_isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(portName))
|
||||
throw new ArgumentNullException(nameof(portName));
|
||||
|
||||
@@ -268,7 +269,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
try
|
||||
{
|
||||
// Get next request to process
|
||||
var item = await _requestQueue.DequeueAsync(cancellationToken);
|
||||
var item = await _requestQueue.DequeueAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
// Remove registration => already removed from queue
|
||||
item.CancellationTokenRegistration.Dispose();
|
||||
@@ -276,13 +277,13 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
// Build combined cancellation token
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
|
||||
// Wait for exclusive access
|
||||
await _portLock.WaitAsync(linkedCts.Token);
|
||||
await _portLock.WaitAsync(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
try
|
||||
{
|
||||
// Ensure connection is up
|
||||
await AssertConnection(linkedCts.Token);
|
||||
|
||||
await _serialPort.WriteAsync(item.Request, linkedCts.Token);
|
||||
await _serialPort.WriteAsync(item.Request, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
linkedCts.Token.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -291,7 +292,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
|
||||
do
|
||||
{
|
||||
int readCount = await _serialPort.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token);
|
||||
int readCount = await _serialPort.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (readCount < 1)
|
||||
throw new EndOfStreamException();
|
||||
|
||||
@@ -322,7 +323,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
_portLock.Release();
|
||||
_idleTimer.Change(IdleTimeout, Timeout.InfiniteTimeSpan);
|
||||
|
||||
await Task.Delay(InterRequestDelay, cancellationToken);
|
||||
await Task.Delay(InterRequestDelay, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
@@ -353,7 +354,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
_serialPort.Close();
|
||||
_serialPort.ResetRS485DriverStateFlags();
|
||||
|
||||
if (DriverEnabledRS485 && (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || _isUnitTest))
|
||||
if (DriverEnabledRS485 && _isLinux)
|
||||
{
|
||||
var flags = _serialPort.GetRS485DriverStateFlags();
|
||||
flags |= RS485Flags.Enabled;
|
||||
@@ -361,7 +362,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
_serialPort.ChangeRS485DriverStateFlags(flags);
|
||||
}
|
||||
|
||||
using var connectTask = Task.Run(_serialPort.Open);
|
||||
using var connectTask = Task.Run(_serialPort.Open, cancellationToken);
|
||||
if (await Task.WhenAny(connectTask, Task.Delay(ReadTimeout, cancellationToken)) == connectTask)
|
||||
{
|
||||
await connectTask;
|
||||
@@ -379,7 +380,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken);
|
||||
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
}
|
||||
catch
|
||||
{ /* keep it quiet */ }
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace System.IO
|
||||
int offset = 0;
|
||||
do
|
||||
{
|
||||
int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken);
|
||||
int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (count < 1)
|
||||
throw new EndOfStreamException();
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace System.IO
|
||||
int offset = 0;
|
||||
do
|
||||
{
|
||||
int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken);
|
||||
int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (count < 1)
|
||||
throw new EndOfStreamException();
|
||||
|
||||
|
||||
17
AMWD.Protocols.Modbus.Tcp/Extensions/TaskExtensions.cs
Normal file
17
AMWD.Protocols.Modbus.Tcp/Extensions/TaskExtensions.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tcp.Extensions
|
||||
{
|
||||
internal static class TaskExtensions
|
||||
{
|
||||
public static async void Forget(this Task task)
|
||||
{
|
||||
try
|
||||
{
|
||||
await task;
|
||||
}
|
||||
catch
|
||||
{ /* keep it quiet */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,7 +208,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
try
|
||||
{
|
||||
// Get next request to process
|
||||
var item = await _requestQueue.DequeueAsync(cancellationToken);
|
||||
var item = await _requestQueue.DequeueAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
// Remove registration => already removed from queue
|
||||
item.CancellationTokenRegistration.Dispose();
|
||||
@@ -216,19 +216,19 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
// Build combined cancellation token
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
|
||||
// Wait for exclusive access
|
||||
await _clientLock.WaitAsync(linkedCts.Token);
|
||||
await _clientLock.WaitAsync(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
try
|
||||
{
|
||||
// Ensure connection is up
|
||||
await AssertConnection(linkedCts.Token);
|
||||
await AssertConnection(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
var stream = _tcpClient.GetStream();
|
||||
await stream.FlushAsync(linkedCts.Token);
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
await stream.WriteAsync(item.Request, linkedCts.Token);
|
||||
await stream.WriteAsync(item.Request, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
#else
|
||||
await stream.WriteAsync(item.Request, 0, item.Request.Length, linkedCts.Token);
|
||||
await stream.WriteAsync(item.Request, 0, item.Request.Length, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
#endif
|
||||
|
||||
linkedCts.Token.ThrowIfCancellationRequested();
|
||||
@@ -239,9 +239,9 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
do
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
int readCount = await stream.ReadAsync(buffer, linkedCts.Token);
|
||||
int readCount = await stream.ReadAsync(buffer, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
#else
|
||||
int readCount = await stream.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token);
|
||||
int readCount = await stream.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
#endif
|
||||
if (readCount < 1)
|
||||
throw new EndOfStreamException();
|
||||
@@ -332,7 +332,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken);
|
||||
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
}
|
||||
catch
|
||||
{ /* keep it quiet */ }
|
||||
|
||||
@@ -10,6 +10,7 @@ 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.Tcp.Extensions;
|
||||
using AMWD.Protocols.Modbus.Tcp.Utils;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tcp
|
||||
@@ -17,7 +18,12 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
/// <summary>
|
||||
/// Implements a Modbus TCP server proxying all requests to a Modbus client of choice.
|
||||
/// </summary>
|
||||
public class ModbusTcpProxy : IModbusProxy
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="ModbusTcpProxy"/> class.
|
||||
/// </remarks>
|
||||
/// <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.</param>
|
||||
public class ModbusTcpProxy(ModbusClientBase client, IPAddress listenAddress) : IModbusProxy
|
||||
{
|
||||
#region Fields
|
||||
|
||||
@@ -25,30 +31,17 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
|
||||
private TimeSpan _readWriteTimeout = TimeSpan.FromSeconds(100);
|
||||
|
||||
private TcpListenerWrapper _tcpListener;
|
||||
private readonly TcpListenerWrapper _tcpListener = new(listenAddress, 502);
|
||||
private CancellationTokenSource _stopCts;
|
||||
private Task _clientConnectTask = Task.CompletedTask;
|
||||
|
||||
private readonly SemaphoreSlim _clientListLock = new(1, 1);
|
||||
private readonly List<TcpClientWrapper> _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.</param>
|
||||
public ModbusTcpProxy(ModbusClientBase client, IPAddress listenAddress)
|
||||
{
|
||||
Client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
|
||||
_tcpListener = new TcpListenerWrapper(listenAddress, 502);
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Properties
|
||||
@@ -56,7 +49,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
/// <summary>
|
||||
/// Gets the Modbus client used to request the remote device, that should be proxied.
|
||||
/// </summary>
|
||||
public ModbusClientBase Client { get; }
|
||||
public ModbusClientBase Client { get; } = client ?? throw new ArgumentNullException(nameof(client));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IPAddress"/> to listen on.
|
||||
@@ -140,16 +133,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
|
||||
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));
|
||||
await Task.WhenAny(_clientConnectTask, Task.Delay(Timeout.Infinite, cancellationToken)).ConfigureAwait(continueOnCapturedContext: false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -174,6 +158,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
_tcpListener.Dispose();
|
||||
|
||||
_stopCts?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Assertions()
|
||||
@@ -196,12 +181,13 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = await _tcpListener.AcceptTcpClientAsync(cancellationToken);
|
||||
await _clientListLock.WaitAsync(cancellationToken);
|
||||
var client = await _tcpListener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
try
|
||||
{
|
||||
_clients.Add(client);
|
||||
_clientTasks.Add(HandleClientAsync(client, cancellationToken));
|
||||
// Can be ignored as it will terminate by itself on cancellation
|
||||
HandleClientAsync(client, cancellationToken).Forget();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -227,20 +213,20 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
using (var cts = new CancellationTokenSource(ReadWriteTimeout))
|
||||
using (cancellationToken.Register(cts.Cancel))
|
||||
{
|
||||
byte[] headerBytes = await stream.ReadExpectedBytesAsync(6, cts.Token);
|
||||
byte[] headerBytes = await stream.ReadExpectedBytesAsync(6, cts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
requestBytes.AddRange(headerBytes);
|
||||
|
||||
ushort length = headerBytes
|
||||
.Skip(4).Take(2).ToArray()
|
||||
.GetBigEndianUInt16();
|
||||
|
||||
byte[] bodyBytes = await stream.ReadExpectedBytesAsync(length, cts.Token);
|
||||
byte[] bodyBytes = await stream.ReadExpectedBytesAsync(length, cts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
requestBytes.AddRange(bodyBytes);
|
||||
}
|
||||
|
||||
byte[] responseBytes = await HandleRequestAsync([.. requestBytes], cancellationToken);
|
||||
byte[] responseBytes = await HandleRequestAsync([.. requestBytes], cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (responseBytes != null)
|
||||
await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken);
|
||||
await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -249,7 +235,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _clientListLock.WaitAsync(cancellationToken);
|
||||
await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
try
|
||||
{
|
||||
_clients.Remove(client);
|
||||
@@ -324,7 +310,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
responseBytes.AddRange(requestBytes.Take(8));
|
||||
try
|
||||
{
|
||||
var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken);
|
||||
var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)];
|
||||
for (int i = 0; i < coils.Count; i++)
|
||||
@@ -363,7 +349,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
responseBytes.AddRange(requestBytes.Take(8));
|
||||
try
|
||||
{
|
||||
var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken);
|
||||
var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)];
|
||||
for (int i = 0; i < discreteInputs.Count; i++)
|
||||
@@ -402,7 +388,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
responseBytes.AddRange(requestBytes.Take(8));
|
||||
try
|
||||
{
|
||||
var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken);
|
||||
var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
byte[] values = new byte[holdingRegisters.Count * 2];
|
||||
for (int i = 0; i < holdingRegisters.Count; i++)
|
||||
@@ -436,7 +422,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
responseBytes.AddRange(requestBytes.Take(8));
|
||||
try
|
||||
{
|
||||
var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken);
|
||||
var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
byte[] values = new byte[count * 2];
|
||||
for (int i = 0; i < count; i++)
|
||||
@@ -484,7 +470,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
LowByte = requestBytes[11],
|
||||
};
|
||||
|
||||
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[6], coil, cancellationToken);
|
||||
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[6], coil, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (isSuccess)
|
||||
{
|
||||
// Response is an echo of the request
|
||||
@@ -524,7 +510,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
LowByte = requestBytes[11]
|
||||
};
|
||||
|
||||
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[6], register, cancellationToken);
|
||||
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[6], register, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (isSuccess)
|
||||
{
|
||||
// Response is an echo of the request
|
||||
@@ -584,7 +570,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
});
|
||||
}
|
||||
|
||||
bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[6], coils, cancellationToken);
|
||||
bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[6], coils, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (isSuccess)
|
||||
{
|
||||
// Response is an echo of the request
|
||||
@@ -641,7 +627,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
});
|
||||
}
|
||||
|
||||
bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[6], list, cancellationToken);
|
||||
bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[6], list, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (isSuccess)
|
||||
{
|
||||
// Response is an echo of the request
|
||||
@@ -698,7 +684,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
|
||||
try
|
||||
{
|
||||
var deviceInfo = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken);
|
||||
var deviceInfo = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
var bodyBytes = new List<byte>();
|
||||
|
||||
@@ -761,7 +747,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] GetDeviceObject(byte objectId, DeviceIdentification deviceIdentification)
|
||||
private static byte[] GetDeviceObject(byte objectId, DeviceIdentification deviceIdentification)
|
||||
{
|
||||
var result = new List<byte> { objectId };
|
||||
switch ((ModbusDeviceIdentificationObject)objectId)
|
||||
@@ -851,5 +837,18 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
}
|
||||
|
||||
#endregion Request Handling
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine($"TCP Proxy");
|
||||
sb.AppendLine($" {nameof(ListenAddress)}: {ListenAddress}");
|
||||
sb.AppendLine($" {nameof(ListenPort)}: {ListenPort}");
|
||||
sb.AppendLine($" {nameof(Client)}: {Client.GetType().Name}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ using System.Net.Sockets;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory for creating <see cref="TcpClientWrapper"/> instances.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal class TcpClientWrapperFactory
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user