Refactoring connection to use an idle timeout and automatically close the underlying data channel

This commit is contained in:
2024-03-31 22:29:07 +02:00
parent 967d80ff3f
commit a58af4d75f
16 changed files with 812 additions and 1198 deletions

View File

@@ -16,23 +16,14 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
string Name { get; }
/// <summary>
/// Gets a value indicating whether the connection is open.
/// Gets or sets the idle time after that the connection is closed.
/// </summary>
bool IsConnected { get; }
/// <summary>
/// Opens the connection to the remote device.
/// </summary>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
/// <returns>An awaitable <see cref="Task"/>.</returns>
Task ConnectAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Closes the connection to the remote device.
/// </summary>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
/// <returns>An awaitable <see cref="Task"/>.</returns>
Task DisconnectAsync(CancellationToken cancellationToken = default);
/// <remarks>
/// Set to <see cref="Timeout.InfiniteTimeSpan"/> to disable idle closing the connection.
/// <br/>
/// Set to <see cref="TimeSpan.Zero"/> to close the connection immediately after each request.
/// </remarks>
TimeSpan IdleTimeout { get; set; }
/// <summary>
/// Invokes a Modbus request.

View File

@@ -46,11 +46,6 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
this.disposeConnection = disposeConnection;
}
/// <summary>
/// Gets a value indicating whether the client is connected.
/// </summary>
public bool IsConnected => connection.IsConnected;
/// <summary>
/// Gets or sets the protocol type to use.
/// </summary>
@@ -59,28 +54,6 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
/// </remarks>
public abstract IModbusProtocol Protocol { get; set; }
/// <summary>
/// Starts the connection to the remote endpoint.
/// </summary>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
/// <returns>An awaitable <see cref="Task"/>.</returns>
public virtual Task ConnectAsync(CancellationToken cancellationToken = default)
{
Assertions(false);
return connection.ConnectAsync(cancellationToken);
}
/// <summary>
/// Stops the connection to the remote endpoint.
/// </summary>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
/// <returns>An awaitable <see cref="Task"/>.</returns>
public virtual Task DisconnectAsync(CancellationToken cancellationToken = default)
{
Assertions(false);
return connection.DisconnectAsync(cancellationToken);
}
/// <summary>
/// Reads multiple <see cref="Coil"/>s.
/// </summary>
@@ -222,31 +195,31 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
switch ((ModbusDeviceIdentificationObject)item.Key)
{
case ModbusDeviceIdentificationObject.VendorName:
devIdent.VendorName = Encoding.ASCII.GetString(item.Value);
devIdent.VendorName = Encoding.UTF8.GetString(item.Value);
break;
case ModbusDeviceIdentificationObject.ProductCode:
devIdent.ProductCode = Encoding.ASCII.GetString(item.Value);
devIdent.ProductCode = Encoding.UTF8.GetString(item.Value);
break;
case ModbusDeviceIdentificationObject.MajorMinorRevision:
devIdent.MajorMinorRevision = Encoding.ASCII.GetString(item.Value);
devIdent.MajorMinorRevision = Encoding.UTF8.GetString(item.Value);
break;
case ModbusDeviceIdentificationObject.VendorUrl:
devIdent.VendorUrl = Encoding.ASCII.GetString(item.Value);
devIdent.VendorUrl = Encoding.UTF8.GetString(item.Value);
break;
case ModbusDeviceIdentificationObject.ProductName:
devIdent.ProductName = Encoding.ASCII.GetString(item.Value);
devIdent.ProductName = Encoding.UTF8.GetString(item.Value);
break;
case ModbusDeviceIdentificationObject.ModelName:
devIdent.ModelName = Encoding.ASCII.GetString(item.Value);
devIdent.ModelName = Encoding.UTF8.GetString(item.Value);
break;
case ModbusDeviceIdentificationObject.UserApplicationName:
devIdent.UserApplicationName = Encoding.ASCII.GetString(item.Value);
devIdent.UserApplicationName = Encoding.UTF8.GetString(item.Value);
break;
default:
@@ -375,7 +348,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
/// <summary>
/// Performs basic assertions.
/// </summary>
protected virtual void Assertions(bool checkConnected = true)
protected virtual void Assertions()
{
#if NET8_0_OR_GREATER
ObjectDisposedException.ThrowIf(_isDisposed, this);
@@ -390,12 +363,6 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
if (Protocol == null)
throw new ArgumentNullException(nameof(Protocol));
#endif
if (!checkConnected)
return;
if (!IsConnected)
throw new ApplicationException($"Connection is not open");
}
}
}

View File

@@ -6,7 +6,8 @@ This package contains all basic tools to build your own clients.
**IModbusConnection**
This is the interface used on the base client to communicate with the remote device.
If you want to use a custom connection type, you should implement this interface yourself.
If you want to use a custom connection type, you should implement this interface yourself.
The `IModbusConnection` is responsible to open and close the data channel in the background.
**IModbusProtocol**
If you want to speak a custom type of protocol with the clients, you can implement this interface.

View File

@@ -0,0 +1,123 @@
using System.Threading;
using System.Threading.Tasks;
namespace System.Collections.Generic
{
// ============================================================================================================================= //
// Source: https://git.am-wd.de/am-wd/common/-/blob/d4b390ad911ce302cc371bb2121fa9c31db1674a/AMWD.Common/Utilities/AsyncQueue.cs //
// ============================================================================================================================= //
[Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal class AsyncQueue<T>
{
private readonly Queue<T> _queue = new();
private TaskCompletionSource<bool> _dequeueTcs = new();
private readonly TaskCompletionSource<bool> _availableTcs = new();
public T Dequeue()
{
lock (_queue)
{
return _queue.Dequeue();
}
}
public void Enqueue(T item)
{
lock (_queue)
{
_queue.Enqueue(item);
SetToken(_dequeueTcs);
SetToken(_availableTcs);
}
}
public async Task<T> DequeueAsync(CancellationToken cancellationToken = default)
{
while (true)
{
TaskCompletionSource<bool> internalDequeueTcs;
lock (_queue)
{
if (_queue.Count > 0)
return _queue.Dequeue();
internalDequeueTcs = ResetToken(ref _dequeueTcs);
}
await WaitAsync(internalDequeueTcs, cancellationToken).ConfigureAwait(false);
}
}
public bool TryDequeue(out T result)
{
try
{
result = Dequeue();
return true;
}
catch
{
result = default;
return false;
}
}
public bool Remove(T item)
{
lock (_queue)
{
var copy = new Queue<T>(_queue);
_queue.Clear();
bool found = false;
int count = copy.Count;
for (int i = 0; i < count; i++)
{
var element = copy.Dequeue();
if (found)
{
_queue.Enqueue(element);
continue;
}
if ((element == null && item == null) || element?.Equals(item) == true)
{
found = true;
continue;
}
_queue.Enqueue(element);
}
return found;
}
}
private static void SetToken(TaskCompletionSource<bool> tcs)
{
tcs.TrySetResult(true);
}
private static TaskCompletionSource<bool> ResetToken(ref TaskCompletionSource<bool> tcs)
{
if (tcs.Task.IsCompleted)
{
tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
}
return tcs;
}
private static async Task WaitAsync(TaskCompletionSource<bool> tcs, CancellationToken cancellationToken)
{
if (await Task.WhenAny(tcs.Task, Task.Delay(-1, cancellationToken)) == tcs.Task)
{
await tcs.Task.ConfigureAwait(false);
return;
}
cancellationToken.ThrowIfCancellationRequested();
}
}
}