diff --git a/AMWD.Protocols.Modbus.Common/Contracts/IModbusConnection.cs b/AMWD.Protocols.Modbus.Common/Contracts/IModbusConnection.cs index fe609e0..ad9bd3e 100644 --- a/AMWD.Protocols.Modbus.Common/Contracts/IModbusConnection.cs +++ b/AMWD.Protocols.Modbus.Common/Contracts/IModbusConnection.cs @@ -25,6 +25,21 @@ namespace AMWD.Protocols.Modbus.Common.Contracts /// TimeSpan IdleTimeout { get; set; } + /// + /// Gets or sets the maximum time until the connect attempt is given up. + /// + TimeSpan ConnectTimeout { get; set; } + + /// + /// Gets or sets the receive time out value of the connection. + /// + TimeSpan ReadTimeout { get; set; } + + /// + /// Gets or sets the send time out value of the connection. + /// + TimeSpan WriteTimeout { get; set; } + /// /// Invokes a Modbus request. /// diff --git a/AMWD.Protocols.Modbus.Tcp/Utils/RequestQueueItem.cs b/AMWD.Protocols.Modbus.Common/Utils/RequestQueueItem.cs similarity index 86% rename from AMWD.Protocols.Modbus.Tcp/Utils/RequestQueueItem.cs rename to AMWD.Protocols.Modbus.Common/Utils/RequestQueueItem.cs index fd53bb1..daf24d0 100644 --- a/AMWD.Protocols.Modbus.Tcp/Utils/RequestQueueItem.cs +++ b/AMWD.Protocols.Modbus.Common/Utils/RequestQueueItem.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; -using System.Text; using System.Threading; using System.Threading.Tasks; -namespace AMWD.Protocols.Modbus.Tcp.Utils +namespace AMWD.Protocols.Modbus.Common.Utils { internal class RequestQueueItem { diff --git a/AMWD.Protocols.Modbus.Tcp/AMWD.Protocols.Modbus.Tcp.csproj b/AMWD.Protocols.Modbus.Tcp/AMWD.Protocols.Modbus.Tcp.csproj index 693f52a..818a888 100644 --- a/AMWD.Protocols.Modbus.Tcp/AMWD.Protocols.Modbus.Tcp.csproj +++ b/AMWD.Protocols.Modbus.Tcp/AMWD.Protocols.Modbus.Tcp.csproj @@ -18,6 +18,7 @@ + diff --git a/AMWD.Protocols.Modbus.Tcp/ModbusTcpClient.cs b/AMWD.Protocols.Modbus.Tcp/ModbusTcpClient.cs index f10cef6..d4c2725 100644 --- a/AMWD.Protocols.Modbus.Tcp/ModbusTcpClient.cs +++ b/AMWD.Protocols.Modbus.Tcp/ModbusTcpClient.cs @@ -27,7 +27,7 @@ namespace AMWD.Protocols.Modbus.Tcp { } /// - /// Initializes a new instance of the class with a specific . + /// Initializes a new instance of the class with a specific . /// /// The responsible for invoking the requests. /// @@ -43,6 +43,34 @@ namespace AMWD.Protocols.Modbus.Tcp /// public override IModbusProtocol Protocol { get; set; } + /// + public TimeSpan IdleTimeout + { + get => connection.IdleTimeout; + set => connection.IdleTimeout = value; + } + + /// + public TimeSpan ConnectTimeout + { + get => connection.ConnectTimeout; + set => connection.ConnectTimeout = value; + } + + /// + public TimeSpan ReadTimeout + { + get => connection.ReadTimeout; + set => connection.ReadTimeout = value; + } + + /// + public TimeSpan WriteTimeout + { + get => connection.WriteTimeout; + set => connection.WriteTimeout = value; + } + /// public string Hostname { @@ -76,73 +104,5 @@ namespace AMWD.Protocols.Modbus.Tcp tcpConnection.Port = value; } } - - /// - public TimeSpan ReadTimeout - { - get - { - if (connection is ModbusTcpConnection tcpConnection) - return tcpConnection.ReadTimeout; - - return default; - } - set - { - if (connection is ModbusTcpConnection tcpConnection) - tcpConnection.ReadTimeout = value; - } - } - - /// - public TimeSpan WriteTimeout - { - get - { - if (connection is ModbusTcpConnection tcpConnection) - return tcpConnection.WriteTimeout; - - return default; - } - set - { - if (connection is ModbusTcpConnection tcpConnection) - tcpConnection.WriteTimeout = value; - } - } - - /// - public TimeSpan ReconnectTimeout - { - get - { - if (connection is ModbusTcpConnection tcpConnection) - return tcpConnection.ConnectTimeout; - - return default; - } - set - { - if (connection is ModbusTcpConnection tcpConnection) - tcpConnection.ConnectTimeout = value; - } - } - - /// - public TimeSpan IdleTimeout - { - get - { - if (connection is ModbusTcpConnection tcpConnection) - return tcpConnection.IdleTimeout; - - return default; - } - set - { - if (connection is ModbusTcpConnection tcpConnection) - tcpConnection.IdleTimeout = value; - } - } } } diff --git a/AMWD.Protocols.Modbus.Tcp/ModbusTcpConnection.cs b/AMWD.Protocols.Modbus.Tcp/ModbusTcpConnection.cs index 6992b16..a24ba6c 100644 --- a/AMWD.Protocols.Modbus.Tcp/ModbusTcpConnection.cs +++ b/AMWD.Protocols.Modbus.Tcp/ModbusTcpConnection.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using AMWD.Protocols.Modbus.Common.Contracts; using AMWD.Protocols.Modbus.Common.Protocols; +using AMWD.Protocols.Modbus.Common.Utils; using AMWD.Protocols.Modbus.Tcp.Utils; namespace AMWD.Protocols.Modbus.Tcp @@ -26,7 +27,7 @@ namespace AMWD.Protocols.Modbus.Tcp private readonly CancellationTokenSource _disposeCts = new(); private readonly SemaphoreSlim _clientLock = new(1, 1); - private readonly TcpClientWrapper _client = new(); + private readonly TcpClientWrapper _tcpClient = new(); private readonly Timer _idleTimer; private readonly Task _processingTask; @@ -49,7 +50,24 @@ namespace AMWD.Protocols.Modbus.Tcp public string Name => "TCP"; /// - public virtual TimeSpan IdleTimeout { get; set; } = TimeSpan.FromSeconds(6); + public virtual TimeSpan IdleTimeout { get; set; } = TimeSpan.FromSeconds(6); + + /// + public virtual TimeSpan ConnectTimeout { get; set; } = TimeSpan.MaxValue; + + /// + public virtual TimeSpan ReadTimeout + { + get => TimeSpan.FromMilliseconds(_tcpClient.ReceiveTimeout); + set => _tcpClient.ReceiveTimeout = (int)value.TotalMilliseconds; + } + + /// + public virtual TimeSpan WriteTimeout + { + get => TimeSpan.FromMilliseconds(_tcpClient.SendTimeout); + set => _tcpClient.SendTimeout = (int)value.TotalMilliseconds; + } /// /// The DNS name of the remote host to which the connection is intended to. @@ -81,32 +99,11 @@ namespace AMWD.Protocols.Modbus.Tcp } } - /// - /// Gets or sets the receive time out value of the connection. - /// - public virtual TimeSpan ReadTimeout - { - get => TimeSpan.FromMilliseconds(_client.ReceiveTimeout); - set => _client.ReceiveTimeout = (int)value.TotalMilliseconds; - } - - /// - /// Gets or sets the send time out value of the connection. - /// - public virtual TimeSpan WriteTimeout - { - get => TimeSpan.FromMilliseconds(_client.SendTimeout); - set => _client.SendTimeout = (int)value.TotalMilliseconds; - } - - /// - /// Gets or sets the maximum time until the connect attempt is given up. - /// - public virtual TimeSpan ConnectTimeout { get; set; } = TimeSpan.MaxValue; - #endregion Properties - /// + /// + /// Releases all managed and unmanaged resources used by the . + /// public void Dispose() { if (_isDisposed) @@ -127,7 +124,7 @@ namespace AMWD.Protocols.Modbus.Tcp OnIdleTimer(null); - _client.Dispose(); + _tcpClient.Dispose(); _clientLock.Dispose(); while (_requestQueue.TryDequeue(out var item)) @@ -204,7 +201,7 @@ namespace AMWD.Protocols.Modbus.Tcp // Ensure connection is up await AssertConnection(linkedCts.Token).ConfigureAwait(false); - var stream = _client.GetStream(); + var stream = _tcpClient.GetStream(); await stream.FlushAsync(linkedCts.Token).ConfigureAwait(false); #if NET6_0_OR_GREATER @@ -270,7 +267,7 @@ namespace AMWD.Protocols.Modbus.Tcp // Has to be called within _clientLock! private async Task AssertConnection(CancellationToken cancellationToken) { - if (_client.Connected) + if (_tcpClient.Connected) return; int delay = 1; @@ -287,17 +284,17 @@ namespace AMWD.Protocols.Modbus.Tcp { foreach (var ipAddress in ipAddresses) { - _client.Close(); + _tcpClient.Close(); #if NET6_0_OR_GREATER - using var connectTask = _client.ConnectAsync(ipAddress, Port, cancellationToken); + using var connectTask = _tcpClient.ConnectAsync(ipAddress, Port, cancellationToken); #else - using var connectTask = _client.ConnectAsync(ipAddress, Port); + using var connectTask = _tcpClient.ConnectAsync(ipAddress, Port); #endif if (await Task.WhenAny(connectTask, Task.Delay(ReadTimeout, cancellationToken)) == connectTask) { await connectTask; - if (_client.Connected) + if (_tcpClient.Connected) return; } } @@ -327,10 +324,10 @@ namespace AMWD.Protocols.Modbus.Tcp _clientLock.Wait(_disposeCts.Token); try { - if (!_client.Connected) + if (!_tcpClient.Connected) return; - _client.Close(); + _tcpClient.Close(); } finally { diff --git a/AMWD.Protocols.Modbus.Tests/Tcp/ModbusTcpClientTest.cs b/AMWD.Protocols.Modbus.Tests/Tcp/ModbusTcpClientTest.cs index e4bc49c..1c3e2c9 100644 --- a/AMWD.Protocols.Modbus.Tests/Tcp/ModbusTcpClientTest.cs +++ b/AMWD.Protocols.Modbus.Tests/Tcp/ModbusTcpClientTest.cs @@ -1,5 +1,4 @@ -using AMWD.Protocols.Modbus.Common.Contracts; -using AMWD.Protocols.Modbus.Tcp; +using AMWD.Protocols.Modbus.Tcp; using Moq; namespace AMWD.Protocols.Modbus.Tests.Tcp @@ -14,13 +13,20 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp public void Initialize() { _genericConnectionMock = new Mock(); + _genericConnectionMock.Setup(c => c.IdleTimeout).Returns(TimeSpan.FromSeconds(40)); + _genericConnectionMock.Setup(c => c.ConnectTimeout).Returns(TimeSpan.FromSeconds(30)); + _genericConnectionMock.Setup(c => c.ReadTimeout).Returns(TimeSpan.FromSeconds(20)); + _genericConnectionMock.Setup(c => c.WriteTimeout).Returns(TimeSpan.FromSeconds(10)); + _tcpConnectionMock = new Mock(); + + _tcpConnectionMock.Setup(c => c.IdleTimeout).Returns(TimeSpan.FromSeconds(10)); + _tcpConnectionMock.Setup(c => c.ConnectTimeout).Returns(TimeSpan.FromSeconds(20)); + _tcpConnectionMock.Setup(c => c.ReadTimeout).Returns(TimeSpan.FromSeconds(30)); + _tcpConnectionMock.Setup(c => c.WriteTimeout).Returns(TimeSpan.FromSeconds(40)); + _tcpConnectionMock.Setup(c => c.Hostname).Returns("127.0.0.1"); _tcpConnectionMock.Setup(c => c.Port).Returns(502); - _tcpConnectionMock.Setup(c => c.ReadTimeout).Returns(TimeSpan.FromSeconds(10)); - _tcpConnectionMock.Setup(c => c.WriteTimeout).Returns(TimeSpan.FromSeconds(20)); - _tcpConnectionMock.Setup(c => c.ConnectTimeout).Returns(TimeSpan.FromSeconds(30)); - _tcpConnectionMock.Setup(c => c.IdleTimeout).Returns(TimeSpan.FromSeconds(40)); } [TestMethod] @@ -32,18 +38,10 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp // Act string hostname = client.Hostname; int port = client.Port; - TimeSpan readTimeout = client.ReadTimeout; - TimeSpan writeTimeout = client.WriteTimeout; - TimeSpan reconnectTimeout = client.ReconnectTimeout; - TimeSpan idleTimeout = client.IdleTimeout; // Assert Assert.IsNull(hostname); Assert.AreEqual(0, port); - Assert.AreEqual(TimeSpan.Zero, readTimeout); - Assert.AreEqual(TimeSpan.Zero, writeTimeout); - Assert.AreEqual(TimeSpan.Zero, reconnectTimeout); - Assert.AreEqual(TimeSpan.Zero, idleTimeout); _genericConnectionMock.VerifyNoOtherCalls(); } @@ -57,17 +55,59 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp // Act client.Hostname = "localhost"; client.Port = 205; - client.ReadTimeout = TimeSpan.FromSeconds(123); - client.WriteTimeout = TimeSpan.FromSeconds(456); - client.ReconnectTimeout = TimeSpan.FromSeconds(789); - client.IdleTimeout = TimeSpan.FromSeconds(321); // Assert _genericConnectionMock.VerifyNoOtherCalls(); } [TestMethod] - public void ShouldReturnValuesForTcpConnection() + public void ShouldReturnValuesForGenericConnection() + { + // Arrange + var client = new ModbusTcpClient(_genericConnectionMock.Object); + + // Act + var idleTimeout = client.IdleTimeout; + var connectTimeout = client.ConnectTimeout; + var readTimeout = client.ReadTimeout; + var writeTimeout = client.WriteTimeout; + + // Assert + Assert.AreEqual(TimeSpan.FromSeconds(40), idleTimeout); + Assert.AreEqual(TimeSpan.FromSeconds(30), connectTimeout); + Assert.AreEqual(TimeSpan.FromSeconds(20), readTimeout); + Assert.AreEqual(TimeSpan.FromSeconds(10), writeTimeout); + + _genericConnectionMock.VerifyGet(c => c.IdleTimeout, Times.Once); + _genericConnectionMock.VerifyGet(c => c.ConnectTimeout, Times.Once); + _genericConnectionMock.VerifyGet(c => c.ReadTimeout, Times.Once); + _genericConnectionMock.VerifyGet(c => c.WriteTimeout, Times.Once); + _genericConnectionMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public void ShouldSetValuesForGenericConnection() + { + // Arrange + var client = new ModbusTcpClient(_genericConnectionMock.Object); + + // Act + client.IdleTimeout = TimeSpan.FromSeconds(10); + client.ConnectTimeout = TimeSpan.FromSeconds(20); + client.ReadTimeout = TimeSpan.FromSeconds(30); + client.WriteTimeout = TimeSpan.FromSeconds(40); + + // Assert + _genericConnectionMock.VerifySet(c => c.IdleTimeout = TimeSpan.FromSeconds(10), Times.Once); + _genericConnectionMock.VerifySet(c => c.ConnectTimeout = TimeSpan.FromSeconds(20), Times.Once); + _genericConnectionMock.VerifySet(c => c.ReadTimeout = TimeSpan.FromSeconds(30), Times.Once); + _genericConnectionMock.VerifySet(c => c.WriteTimeout = TimeSpan.FromSeconds(40), Times.Once); + + _genericConnectionMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public void ShouldGetValuesForTcpConnection() { // Arrange var client = new ModbusTcpClient(_tcpConnectionMock.Object); @@ -75,25 +115,26 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp // Act string hostname = client.Hostname; int port = client.Port; - TimeSpan readTimeout = client.ReadTimeout; - TimeSpan writeTimeout = client.WriteTimeout; - TimeSpan reconnectTimeout = client.ReconnectTimeout; - TimeSpan keepAliveInterval = client.IdleTimeout; + var idleTimeout = client.IdleTimeout; + var connectTimeout = client.ConnectTimeout; + var readTimeout = client.ReadTimeout; + var writeTimeout = client.WriteTimeout; // Assert Assert.AreEqual("127.0.0.1", hostname); Assert.AreEqual(502, port); - Assert.AreEqual(10, readTimeout.TotalSeconds); - Assert.AreEqual(20, writeTimeout.TotalSeconds); - Assert.AreEqual(30, reconnectTimeout.TotalSeconds); - Assert.AreEqual(40, keepAliveInterval.TotalSeconds); + Assert.AreEqual(TimeSpan.FromSeconds(10), idleTimeout); + Assert.AreEqual(TimeSpan.FromSeconds(20), connectTimeout); + Assert.AreEqual(TimeSpan.FromSeconds(30), readTimeout); + Assert.AreEqual(TimeSpan.FromSeconds(40), writeTimeout); _tcpConnectionMock.VerifyGet(c => c.Hostname, Times.Once); _tcpConnectionMock.VerifyGet(c => c.Port, Times.Once); + _tcpConnectionMock.VerifyGet(c => c.IdleTimeout, Times.Once); + _tcpConnectionMock.VerifyGet(c => c.ConnectTimeout, Times.Once); _tcpConnectionMock.VerifyGet(c => c.ReadTimeout, Times.Once); _tcpConnectionMock.VerifyGet(c => c.WriteTimeout, Times.Once); - _tcpConnectionMock.VerifyGet(c => c.ConnectTimeout, Times.Once); - _tcpConnectionMock.VerifyGet(c => c.IdleTimeout, Times.Once); + _tcpConnectionMock.VerifyNoOtherCalls(); } @@ -106,18 +147,19 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp // Act client.Hostname = "localhost"; client.Port = 205; - client.ReadTimeout = TimeSpan.FromSeconds(123); - client.WriteTimeout = TimeSpan.FromSeconds(456); - client.ReconnectTimeout = TimeSpan.FromSeconds(789); - client.IdleTimeout = TimeSpan.FromSeconds(321); + client.IdleTimeout = TimeSpan.FromSeconds(40); + client.ConnectTimeout = TimeSpan.FromSeconds(30); + client.ReadTimeout = TimeSpan.FromSeconds(20); + client.WriteTimeout = TimeSpan.FromSeconds(10); // Assert _tcpConnectionMock.VerifySet(c => c.Hostname = "localhost", Times.Once); _tcpConnectionMock.VerifySet(c => c.Port = 205, Times.Once); - _tcpConnectionMock.VerifySet(c => c.ReadTimeout = TimeSpan.FromSeconds(123), Times.Once); - _tcpConnectionMock.VerifySet(c => c.WriteTimeout = TimeSpan.FromSeconds(456), Times.Once); - _tcpConnectionMock.VerifySet(c => c.ConnectTimeout = TimeSpan.FromSeconds(789), Times.Once); - _tcpConnectionMock.VerifySet(c => c.IdleTimeout = TimeSpan.FromSeconds(321), Times.Once); + _tcpConnectionMock.VerifySet(c => c.IdleTimeout = TimeSpan.FromSeconds(40), Times.Once); + _tcpConnectionMock.VerifySet(c => c.ConnectTimeout = TimeSpan.FromSeconds(30), Times.Once); + _tcpConnectionMock.VerifySet(c => c.ReadTimeout = TimeSpan.FromSeconds(20), Times.Once); + _tcpConnectionMock.VerifySet(c => c.WriteTimeout = TimeSpan.FromSeconds(10), Times.Once); + _tcpConnectionMock.VerifyNoOtherCalls(); } } diff --git a/AMWD.Protocols.Modbus.Tests/Tcp/ModbusTcpConnectionTest.cs b/AMWD.Protocols.Modbus.Tests/Tcp/ModbusTcpConnectionTest.cs index 5ca6cc4..4e2ca66 100644 --- a/AMWD.Protocols.Modbus.Tests/Tcp/ModbusTcpConnectionTest.cs +++ b/AMWD.Protocols.Modbus.Tests/Tcp/ModbusTcpConnectionTest.cs @@ -4,7 +4,6 @@ using System.Net; using System.Reflection; using System.Threading; using System.Threading.Tasks; -using AMWD.Protocols.Modbus.Common.Contracts; using AMWD.Protocols.Modbus.Tcp; using AMWD.Protocols.Modbus.Tcp.Utils; using Moq; @@ -528,18 +527,12 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp Port = 502 }; - // Replace real TCP client with mock - var clientField = connection.GetType().GetField("_client", BindingFlags.NonPublic | BindingFlags.Instance); - (clientField.GetValue(connection) as TcpClientWrapper)?.Dispose(); - clientField.SetValue(connection, _tcpClientMock.Object); + // Replace real connection with mock + var connectionField = connection.GetType().GetField("_tcpClient", BindingFlags.NonPublic | BindingFlags.Instance); + (connectionField.GetValue(connection) as TcpClientWrapper)?.Dispose(); + connectionField.SetValue(connection, _tcpClientMock.Object); return connection; } - - private void ClearInvocations() - { - _networkStreamMock.Invocations.Clear(); - _tcpClientMock.Invocations.Clear(); - } } }