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
{