Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/FluentModbus/Client/ModbusRtuOverTcpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
8 changes: 3 additions & 5 deletions src/FluentModbus/Client/ModbusTcpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down
156 changes: 154 additions & 2 deletions src/FluentModbus/Client/ModbusTcpClientAsync.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,162 @@


/* This is automatically translated code. */

using System.Net;
using System.Net.Sockets;

namespace FluentModbus;

public partial class ModbusTcpClient
{
/// <summary>
/// Connect to localhost at port 502 with <see cref="ModbusEndianness.LittleEndian"/> as default byte layout.
/// </summary>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
public Task ConnectAsync(CancellationToken cancellationToken = default)
{
return ConnectAsync(ModbusEndianness.LittleEndian, cancellationToken);
}

/// <summary>
/// Connect to localhost at port 502.
/// </summary>
/// <param name="endianness">Specifies the endianness of the data exchanged with the Modbus server.</param>
public Task ConnectAsync(ModbusEndianness endianness, CancellationToken cancellationToken = default)
{
return ConnectAsync(new IPEndPoint(IPAddress.Loopback, 502), endianness, cancellationToken);
}

/// <summary>
/// Connect to the specified <paramref name="remoteEndpoint"/>.
/// </summary>
/// <param name="remoteEndpoint">The IP address and optional port of the end unit with <see cref="ModbusEndianness.LittleEndian"/> as default byte layout. Examples: "192.168.0.1", "192.168.0.1:502", "::1", "[::1]:502". The default port is 502.</param>
public Task ConnectAsync(string remoteEndpoint, CancellationToken cancellationToken = default)
{
return ConnectAsync(remoteEndpoint, ModbusEndianness.LittleEndian, cancellationToken);
}

/// <summary>
/// Connect to the specified <paramref name="remoteEndpoint"/>.
/// </summary>
/// <param name="remoteEndpoint">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.</param>
/// <param name="endianness">Specifies the endianness of the data exchanged with the Modbus server.</param>
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
}

/// <summary>
/// Connect to the specified <paramref name="remoteIpAddress"/> at port 502.
/// </summary>
/// <param name="remoteIpAddress">The IP address of the end unit with <see cref="ModbusEndianness.LittleEndian"/> as default byte layout. Example: IPAddress.Parse("192.168.0.1").</param>
public Task ConnectAsync(IPAddress remoteIpAddress, CancellationToken cancellationToken = default)
{
return ConnectAsync(remoteIpAddress, ModbusEndianness.LittleEndian, cancellationToken);
}

/// <summary>
/// Connect to the specified <paramref name="remoteIpAddress"/> at port 502.
/// </summary>
/// <param name="remoteIpAddress">The IP address of the end unit. Example: IPAddress.Parse("192.168.0.1").</param>
/// <param name="endianness">Specifies the endianness of the data exchanged with the Modbus server.</param>
public Task ConnectAsync(IPAddress remoteIpAddress, ModbusEndianness endianness, CancellationToken cancellationToken = default)
{
return ConnectAsync(new IPEndPoint(remoteIpAddress, 502), endianness, cancellationToken);
}

/// <summary>
/// Connect to the specified <paramref name="remoteEndpoint"/> with <see cref="ModbusEndianness.LittleEndian"/> as default byte layout.
/// </summary>
/// <param name="remoteEndpoint">The IP address and port of the end unit.</param>
public Task ConnectAsync(IPEndPoint remoteEndpoint, CancellationToken cancellationToken = default)
{
return ConnectAsync(remoteEndpoint, ModbusEndianness.LittleEndian, cancellationToken);
}

/// <summary>
/// Connect to the specified <paramref name="remoteEndpoint"/>.
/// </summary>
/// <param name="remoteEndpoint">The IP address and port of the end unit.</param>
/// <param name="endianness">Specifies the endianness of the data exchanged with the Modbus server.</param>
public Task ConnectAsync(IPEndPoint remoteEndpoint, ModbusEndianness endianness, CancellationToken cancellationToken = default)
{
return InitializeAsync(new TcpClient(), remoteEndpoint, endianness, cancellationToken);
}

/// <summary>
/// Asynchronously initialize the Modbus TCP client with an externally managed <see cref="TcpClient"/>.
/// </summary>
/// <param name="tcpClient">The externally managed <see cref="TcpClient"/>.</param>
/// <param name="endianness">Specifies the endianness of the data exchanged with the Modbus server.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
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;
}
}

///<inheritdoc/>
protected override async Task<Memory<byte>> TransceiveFrameAsync(byte unitIdentifier, ModbusFunctionCode functionCode, Action<ExtendedBinaryWriter> extendFrame, CancellationToken cancellationToken = default)
{
Expand Down Expand Up @@ -121,5 +273,5 @@ protected override async Task<Memory<byte>> TransceiveFrameAsync(byte unitIdenti
throw new ModbusException(ErrorMessage.ModbusClient_InvalidResponseFunctionCode);

return frameBuffer.Buffer.AsMemory(7, frameLength - 7);
}
}
}
119 changes: 104 additions & 15 deletions src/FluentModbus/Client/ModbusTcpClientAsync.tt
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte> 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<byte> TransceiveFrame\()(.*?)\)(.*?\n })", RegexOptions.Singleline)[0];

// Extract Connect methods - match XML docs (including param tags) + method signature + body
var connectMatches = Regex.Matches(csstring, @" /// <summary>\r?\n(?: /// .*?\r?\n)* /// </summary>\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]*</summary>\r?\n)( public)", "$1 /// <param name=\"cancellationToken\">The token to monitor for cancellation requests.</param>\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");
}
#>
/// <summary>
/// Asynchronously initialize the Modbus TCP client with an externally managed <see cref="TcpClient"/>.
/// </summary>
/// <param name="tcpClient">The externally managed <see cref="TcpClient"/>.</param>
/// <param name="endianness">Specifies the endianness of the data exchanged with the Modbus server.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
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($"///<inheritdoc/>\n protected override async Task<Memory<byte>> 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($" ///<inheritdoc/>\n protected override async Task<Memory<byte>> TransceiveFrameAsync({signature}, CancellationToken cancellationToken = default){body}");
#>

}
7 changes: 6 additions & 1 deletion src/FluentModbus/FluentModbus.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<Description>Lightweight and fast client and server implementation of the Modbus protocol (TCP/RTU, Sync/Async).</Description>
<PackageTags>Modbus ModbusTCP ModbusRTU .NET Standard Windows Linux</PackageTags>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;net5.0</TargetFrameworks>
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Targeting .NET 5 is required for ConnectAsync with cancellation token support .NET 5 or newer.

<PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
Expand All @@ -26,6 +26,11 @@
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="5.0.0" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)'=='net5.0'">
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
<PackageReference Include="System.IO.Ports" Version="5.0.0" />
</ItemGroup>
Comment on lines +29 to +32
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also eliminates a dependency.


<ItemGroup>
<None Include="../../doc/images/icon.png" Pack="true" PackagePath="/" />
</ItemGroup>
Expand Down
5 changes: 2 additions & 3 deletions src/FluentModbus/ModbusUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -13,8 +13,7 @@ internal static class ModbusUtils
{
#if NETSTANDARD2_0
public static bool TryParseEndpoint(ReadOnlySpan<char> value, out IPEndPoint? result)
#endif
#if NETSTANDARD2_1_OR_GREATER
#else
public static bool TryParseEndpoint(ReadOnlySpan<char> value, [NotNullWhen(true)] out IPEndPoint? result)
#endif
{
Expand Down