Refactoring connection to use an idle timeout and automatically close the underlying data channel
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
123
AMWD.Protocols.Modbus.Common/Utils/AsyncQueue.cs
Normal file
123
AMWD.Protocols.Modbus.Common/Utils/AsyncQueue.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user