diff --git a/src/FluentModbus/Client/ModbusRtuOverTcpClient.cs b/src/FluentModbus/Client/ModbusRtuOverTcpClient.cs index eb06337..047b709 100644 --- a/src/FluentModbus/Client/ModbusRtuOverTcpClient.cs +++ b/src/FluentModbus/Client/ModbusRtuOverTcpClient.cs @@ -95,9 +95,7 @@ public void Connect(string remoteEndpoint, ModbusEndianness endianness) #if NETSTANDARD2_0 Connect(parsedRemoteEndpoint!, endianness); - #endif - - #if NETSTANDARD2_1_OR_GREATER + #else Connect(parsedRemoteEndpoint, endianness); #endif } diff --git a/src/FluentModbus/Client/ModbusTcpClient.cs b/src/FluentModbus/Client/ModbusTcpClient.cs index e1cd49c..5ca31ac 100755 --- a/src/FluentModbus/Client/ModbusTcpClient.cs +++ b/src/FluentModbus/Client/ModbusTcpClient.cs @@ -99,9 +99,7 @@ public void Connect(string remoteEndpoint, ModbusEndianness endianness) #if NETSTANDARD2_0 Connect(parsedRemoteEndpoint!, endianness); - #endif - - #if NETSTANDARD2_1_OR_GREATER + #else Connect(parsedRemoteEndpoint, endianness); #endif } @@ -156,7 +154,7 @@ public void Initialize(TcpClient tcpClient, ModbusEndianness endianness) private void Initialize(TcpClient tcpClient, IPEndPoint? remoteEndpoint, ModbusEndianness endianness) { - base.SwapBytes = BitConverter.IsLittleEndian && endianness == ModbusEndianness.BigEndian || + base.SwapBytes = BitConverter.IsLittleEndian && endianness == ModbusEndianness.BigEndian || !BitConverter.IsLittleEndian && endianness == ModbusEndianness.LittleEndian; _frameBuffer = new ModbusFrameBuffer(size: 260); @@ -170,7 +168,7 @@ private void Initialize(TcpClient tcpClient, IPEndPoint? remoteEndpoint, ModbusE if (remoteEndpoint is not null && !tcpClient.ConnectAsync(remoteEndpoint.Address, remoteEndpoint.Port).Wait(ConnectTimeout)) throw new Exception(ErrorMessage.ModbusClient_TcpConnectTimeout); - // Why no method signature with NetworkStream only and then set the timeouts + // Why no method signature with NetworkStream only and then set the timeouts // in the Connect method like for the RTU client? // // "If a NetworkStream was associated with a TcpClient, the Close method will diff --git a/src/FluentModbus/Client/ModbusTcpClientAsync.cs b/src/FluentModbus/Client/ModbusTcpClientAsync.cs index eb89741..be09093 100755 --- a/src/FluentModbus/Client/ModbusTcpClientAsync.cs +++ b/src/FluentModbus/Client/ModbusTcpClientAsync.cs @@ -1,10 +1,162 @@ - + /* This is automatically translated code. */ + +using System.Net; +using System.Net.Sockets; namespace FluentModbus; public partial class ModbusTcpClient { + /// + /// Connect to localhost at port 502 with as default byte layout. + /// + /// The token to monitor for cancellation requests. + public Task ConnectAsync(CancellationToken cancellationToken = default) + { + return ConnectAsync(ModbusEndianness.LittleEndian, cancellationToken); + } + + /// + /// Connect to localhost at port 502. + /// + /// Specifies the endianness of the data exchanged with the Modbus server. + public Task ConnectAsync(ModbusEndianness endianness, CancellationToken cancellationToken = default) + { + return ConnectAsync(new IPEndPoint(IPAddress.Loopback, 502), endianness, cancellationToken); + } + + /// + /// Connect to the specified . + /// + /// The IP address and optional port of the end unit with as default byte layout. Examples: "192.168.0.1", "192.168.0.1:502", "::1", "[::1]:502". The default port is 502. + public Task ConnectAsync(string remoteEndpoint, CancellationToken cancellationToken = default) + { + return ConnectAsync(remoteEndpoint, ModbusEndianness.LittleEndian, cancellationToken); + } + + /// + /// Connect to the specified . + /// + /// The IP address and optional port of the end unit. Examples: "192.168.0.1", "192.168.0.1:502", "::1", "[::1]:502". The default port is 502. + /// Specifies the endianness of the data exchanged with the Modbus server. + public Task ConnectAsync(string remoteEndpoint, ModbusEndianness endianness, CancellationToken cancellationToken = default) + { + if (!ModbusUtils.TryParseEndpoint(remoteEndpoint.AsSpan(), out var parsedRemoteEndpoint)) + throw new FormatException("An invalid IPEndPoint was specified."); + + #if NETSTANDARD2_0 + return ConnectAsync(parsedRemoteEndpoint!, endianness, cancellationToken); + #else + return ConnectAsync(parsedRemoteEndpoint, endianness, cancellationToken); + #endif + } + + /// + /// Connect to the specified at port 502. + /// + /// The IP address of the end unit with as default byte layout. Example: IPAddress.Parse("192.168.0.1"). + public Task ConnectAsync(IPAddress remoteIpAddress, CancellationToken cancellationToken = default) + { + return ConnectAsync(remoteIpAddress, ModbusEndianness.LittleEndian, cancellationToken); + } + + /// + /// Connect to the specified at port 502. + /// + /// The IP address of the end unit. Example: IPAddress.Parse("192.168.0.1"). + /// Specifies the endianness of the data exchanged with the Modbus server. + public Task ConnectAsync(IPAddress remoteIpAddress, ModbusEndianness endianness, CancellationToken cancellationToken = default) + { + return ConnectAsync(new IPEndPoint(remoteIpAddress, 502), endianness, cancellationToken); + } + + /// + /// Connect to the specified with as default byte layout. + /// + /// The IP address and port of the end unit. + public Task ConnectAsync(IPEndPoint remoteEndpoint, CancellationToken cancellationToken = default) + { + return ConnectAsync(remoteEndpoint, ModbusEndianness.LittleEndian, cancellationToken); + } + + /// + /// Connect to the specified . + /// + /// The IP address and port of the end unit. + /// Specifies the endianness of the data exchanged with the Modbus server. + public Task ConnectAsync(IPEndPoint remoteEndpoint, ModbusEndianness endianness, CancellationToken cancellationToken = default) + { + return InitializeAsync(new TcpClient(), remoteEndpoint, endianness, cancellationToken); + } + + /// + /// Asynchronously initialize the Modbus TCP client with an externally managed . + /// + /// The externally managed . + /// Specifies the endianness of the data exchanged with the Modbus server. + /// The token to monitor for cancellation requests. + public Task InitializeAsync(TcpClient tcpClient, ModbusEndianness endianness, CancellationToken cancellationToken = default) + { + return InitializeAsync(tcpClient, default, endianness, cancellationToken); + } + + private async Task InitializeAsync(TcpClient tcpClient, IPEndPoint? remoteEndpoint, ModbusEndianness endianness, CancellationToken cancellationToken) + { + + base.SwapBytes = BitConverter.IsLittleEndian && endianness == ModbusEndianness.BigEndian || + !BitConverter.IsLittleEndian && endianness == ModbusEndianness.LittleEndian; + + _frameBuffer = new ModbusFrameBuffer(size: 260); + + if (_tcpClient.HasValue && _tcpClient.Value.IsInternal) + _tcpClient.Value.Value.Close(); + + var isInternal = remoteEndpoint is not null; + _tcpClient = (tcpClient, isInternal); + + if (remoteEndpoint is not null) + { +#if NET5_0_OR_GREATER + using var timeoutCts = new CancellationTokenSource(ConnectTimeout); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + + try + { + await tcpClient.ConnectAsync(remoteEndpoint.Address, remoteEndpoint.Port, linkedCts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested) + { + throw new TimeoutException(ErrorMessage.ModbusClient_TcpConnectTimeout); + } +#else + var connectTask = tcpClient.ConnectAsync(remoteEndpoint.Address, remoteEndpoint.Port); + var timeoutTask = Task.Delay(ConnectTimeout, cancellationToken); + var completedTask = await Task.WhenAny(connectTask, timeoutTask).ConfigureAwait(false); + + if (completedTask == timeoutTask) + throw new TimeoutException(ErrorMessage.ModbusClient_TcpConnectTimeout); + + await connectTask.ConfigureAwait(false); // Propagate any connection exceptions +#endif + } + + // Why no method signature with NetworkStream only and then set the timeouts + // in the Connect method like for the RTU client? + // + // "If a NetworkStream was associated with a TcpClient, the Close method will + // close the TCP connection, but not dispose of the associated TcpClient." + // -> https://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.networkstream.close?view=net-6.0 + + _networkStream = tcpClient.GetStream(); + + if (isInternal) + { + _networkStream.ReadTimeout = ReadTimeout; + _networkStream.WriteTimeout = WriteTimeout; + } + } + /// protected override async Task> TransceiveFrameAsync(byte unitIdentifier, ModbusFunctionCode functionCode, Action extendFrame, CancellationToken cancellationToken = default) { @@ -121,5 +273,5 @@ protected override async Task> TransceiveFrameAsync(byte unitIdenti throw new ModbusException(ErrorMessage.ModbusClient_InvalidResponseFunctionCode); return frameBuffer.Buffer.AsMemory(7, frameLength - 7); - } + } } \ No newline at end of file diff --git a/src/FluentModbus/Client/ModbusTcpClientAsync.tt b/src/FluentModbus/Client/ModbusTcpClientAsync.tt index 6b7a8f9..abd70cb 100755 --- a/src/FluentModbus/Client/ModbusTcpClientAsync.tt +++ b/src/FluentModbus/Client/ModbusTcpClientAsync.tt @@ -3,28 +3,117 @@ <#@ import namespace="System.IO" #> <#@ import namespace="System.Text.RegularExpressions" #> -<# +<# var csstring = File.ReadAllText("src/FluentModbus/Client/ModbusTcpClient.cs"); - var match = Regex.Matches(csstring, @"(protected override Span TransceiveFrame\()(.*?)\)(.*?\n })", RegexOptions.Singleline)[0]; + + // Extract TransceiveFrame - match until closing brace at method level (4 spaces indentation) + var transceiveMatch = Regex.Matches(csstring, @"(protected override Span TransceiveFrame\()(.*?)\)(.*?\n })", RegexOptions.Singleline)[0]; + + // Extract Connect methods - match XML docs (including param tags) + method signature + body + var connectMatches = Regex.Matches(csstring, @" /// \r?\n(?: /// .*?\r?\n)* /// \r?\n(?: /// .*?\r?\n)* public void Connect\([^\)]*\)\r?\n \{(?:[\s\S]*?)\r?\n \}"); + + // Extract Initialize method + var initializeMatch = Regex.Match(csstring, @" private void Initialize\(TcpClient tcpClient, IPEndPoint\? remoteEndpoint, ModbusEndianness endianness\)\r?\n \{(.*?)\r?\n \}", RegexOptions.Singleline); #> /* This is automatically translated code. */ + +using System.Net; +using System.Net.Sockets; -namespace FluentModbus +namespace FluentModbus; + +public partial class ModbusTcpClient { - public partial class ModbusTcpClient +<# + // Generate Connect async methods + foreach (Match method in connectMatches) + { + var methodString = method.Value; + + // Add cancellation token XML comment before method signature + methodString = Regex.Replace(methodString, @"( ///[^\n]*\r?\n)( public)", "$1 /// The token to monitor for cancellation requests.\r\n$2"); + + // Transform method signature: void Connect(...) -> Task ConnectAsync(..., CancellationToken) + methodString = Regex.Replace(methodString, @"public void Connect\(([^\)]*)\)", m => { + var parameters = m.Groups[1].Value.Trim(); + if (string.IsNullOrEmpty(parameters)) + return "public Task ConnectAsync(CancellationToken cancellationToken = default)"; + else + return $"public Task ConnectAsync({parameters}, CancellationToken cancellationToken = default)"; + }); + + // Replace Connect(...) calls with return ConnectAsync(..., cancellationToken) + // Use a more robust pattern that handles nested parentheses + methodString = Regex.Replace(methodString, @" Connect\(((?:[^()]|\([^()]*\))+)\);", m => $" return ConnectAsync({m.Groups[1]}, cancellationToken);"); + + // Replace Initialize(...) calls with return InitializeAsync(..., cancellationToken) + methodString = Regex.Replace(methodString, @" Initialize\(((?:[^()]|\([^()]*\))+)\);", m => $" return InitializeAsync({m.Groups[1]}, cancellationToken);"); + + Write(methodString); + Write("\r\n\r\n"); + } +#> + /// + /// Asynchronously initialize the Modbus TCP client with an externally managed . + /// + /// The externally managed . + /// Specifies the endianness of the data exchanged with the Modbus server. + /// The token to monitor for cancellation requests. + public Task InitializeAsync(TcpClient tcpClient, ModbusEndianness endianness, CancellationToken cancellationToken = default) { - <# - // replace AsSpan - var signature = match.Groups[2].Value; - var body = match.Groups[3].Value; - body = Regex.Replace(body, "AsSpan", "AsMemory"); - body = Regex.Replace(body, @"_networkStream.Write\((.*?)\)", m => $"await _networkStream.WriteAsync({m.Groups[1]}, cancellationToken).ConfigureAwait(false)"); - body = Regex.Replace(body, @"_networkStream.Read\((.*?)\)", m => $"await _networkStream.ReadAsync({m.Groups[1]}, cancellationToken).ConfigureAwait(false)"); - body = Regex.Replace(body, @"// ASYNC-ONLY: ", ""); - - Write($"///\n protected override async Task> TransceiveFrameAsync({signature}, CancellationToken cancellationToken = default){body}"); - #> + return InitializeAsync(tcpClient, default, endianness, cancellationToken); + } + +<# + // Generate Initialize async method + var initBody = initializeMatch.Groups[1].Value; + // Replace the sync connection code with async version + initBody = Regex.Replace(initBody, + @" if \(remoteEndpoint is not null && !tcpClient\.ConnectAsync\(remoteEndpoint\.Address, remoteEndpoint\.Port\)\.Wait\(ConnectTimeout\)\)\r?\n throw new Exception\(ErrorMessage\.ModbusClient_TcpConnectTimeout\);", + @" if (remoteEndpoint is not null) + { +#if NET5_0_OR_GREATER + using var timeoutCts = new CancellationTokenSource(ConnectTimeout); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + + try + { + await tcpClient.ConnectAsync(remoteEndpoint.Address, remoteEndpoint.Port, linkedCts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested) + { + throw new TimeoutException(ErrorMessage.ModbusClient_TcpConnectTimeout); + } +#else + var connectTask = tcpClient.ConnectAsync(remoteEndpoint.Address, remoteEndpoint.Port); + var timeoutTask = Task.Delay(ConnectTimeout, cancellationToken); + var completedTask = await Task.WhenAny(connectTask, timeoutTask).ConfigureAwait(false); + + if (completedTask == timeoutTask) + throw new TimeoutException(ErrorMessage.ModbusClient_TcpConnectTimeout); + + await connectTask.ConfigureAwait(false); // Propagate any connection exceptions +#endif + }", + RegexOptions.Singleline); +#> + private async Task InitializeAsync(TcpClient tcpClient, IPEndPoint? remoteEndpoint, ModbusEndianness endianness, CancellationToken cancellationToken) + { +<#= initBody #> } + +<# + // Generate TransceiveFrameAsync + var signature = transceiveMatch.Groups[2].Value; + var body = transceiveMatch.Groups[3].Value; + body = Regex.Replace(body, "AsSpan", "AsMemory"); + body = Regex.Replace(body, @"_networkStream.Write\((.*?)\)", m => $"await _networkStream.WriteAsync({m.Groups[1]}, cancellationToken).ConfigureAwait(false)"); + body = Regex.Replace(body, @"_networkStream.Read\((.*?)\)", m => $"await _networkStream.ReadAsync({m.Groups[1]}, cancellationToken).ConfigureAwait(false)"); + body = Regex.Replace(body, @"// ASYNC-ONLY: ", ""); + + Write($" ///\n protected override async Task> TransceiveFrameAsync({signature}, CancellationToken cancellationToken = default){body}"); +#> + } \ No newline at end of file diff --git a/src/FluentModbus/FluentModbus.csproj b/src/FluentModbus/FluentModbus.csproj index f9590bd..818e745 100644 --- a/src/FluentModbus/FluentModbus.csproj +++ b/src/FluentModbus/FluentModbus.csproj @@ -4,7 +4,7 @@ Lightweight and fast client and server implementation of the Modbus protocol (TCP/RTU, Sync/Async). Modbus ModbusTCP ModbusRTU .NET Standard Windows Linux true - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net5.0 icon.png README.md @@ -26,6 +26,11 @@ + + + + + diff --git a/src/FluentModbus/ModbusUtils.cs b/src/FluentModbus/ModbusUtils.cs index 8d42c8f..d0dc621 100644 --- a/src/FluentModbus/ModbusUtils.cs +++ b/src/FluentModbus/ModbusUtils.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -#if NETSTANDARD2_1_OR_GREATER +#if !NETSTANDARD2_0 using System.Diagnostics.CodeAnalysis; #endif @@ -13,8 +13,7 @@ internal static class ModbusUtils { #if NETSTANDARD2_0 public static bool TryParseEndpoint(ReadOnlySpan value, out IPEndPoint? result) -#endif -#if NETSTANDARD2_1_OR_GREATER +#else public static bool TryParseEndpoint(ReadOnlySpan value, [NotNullWhen(true)] out IPEndPoint? result) #endif {