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
2 changes: 1 addition & 1 deletion src/FluentModbus/Client/ModbusClientAsync.tt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
.ReadAllText("src/FluentModbus/Client/ModbusClient.cs")
.Replace("\r\n", "\n");

var methodsToConvert = Regex.Matches(csstring, @" \/{3}(?:.(?!\n\n))*?(?:extendFrame\);|\).*?\n })", RegexOptions.Singleline);
var methodsToConvert = Regex.Matches(csstring, @" \/{3}(?:.(?!\n\n))*?(?:extendFrame\);|\).*?\n })", RegexOptions.Singleline);
#>
/* This is automatically translated code. */

Expand Down
2 changes: 1 addition & 1 deletion src/FluentModbus/Client/ModbusRtuClientAsync.tt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<#
var csstring = File.ReadAllText("src/FluentModbus/Client/ModbusRtuClient.cs");
var match = Regex.Matches(csstring, @"(protected override Span<byte> TransceiveFrame\()(.*?)\)(.*?\n })", RegexOptions.Singleline)[0];
var match = Regex.Matches(csstring, @"(protected override Span<byte> TransceiveFrame\()(.*?)\)(.*?\n })", RegexOptions.Singleline)[0];

#>
/* This is automatically translated code. */
Expand Down
24 changes: 22 additions & 2 deletions src/FluentModbus/Client/ModbusRtuOverTcpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,28 @@ private void Initialize(TcpClient tcpClient, IPEndPoint? remoteEndpoint, ModbusE
var isInternal = remoteEndpoint is not null;
_tcpClient = (tcpClient, isInternal);

if (remoteEndpoint is not null && !tcpClient.ConnectAsync(remoteEndpoint.Address, remoteEndpoint.Port).Wait(ConnectTimeout))
throw new Exception(ErrorMessage.ModbusClient_TcpConnectTimeout);
if (remoteEndpoint is not null)
{
// ASYNC-ONLY: using var timeoutCts = new CancellationTokenSource(ConnectTimeout);
// ASYNC-ONLY: using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
// ASYNC-ONLY:
if (!tcpClient.ConnectAsync(remoteEndpoint.Address, remoteEndpoint.Port).Wait(ConnectTimeout))
// ASYNC-ONLY: var cancellationTask = Task.Delay(-1, linkedCts.Token);
// ASYNC-ONLY:
// ASYNC-ONLY: var completedTask = await Task.WhenAny(connectTask, cancellationTask).ConfigureAwait(false);
// ASYNC-ONLY:
// ASYNC-ONLY: if (completedTask == cancellationTask)
// ASYNC-ONLY: {
// ASYNC-ONLY: tcpClient.Close(); // Cancel the connect attempt
// ASYNC-ONLY:
// ASYNC-ONLY: if (cancellationToken.IsCancellationRequested)
// ASYNC-ONLY: throw new OperationCanceledException(cancellationToken);
// ASYNC-ONLY:
throw new Exception(ErrorMessage.ModbusClient_TcpConnectTimeout);
// ASYNC-ONLY: }
// ASYNC-ONLY:
// ASYNC-ONLY: await connectTask.ConfigureAwait(false); // Surface any connection exceptions
}

// Why no method signature with NetworkStream only and then set the timeouts
// in the Connect method like for the RTU client?
Expand Down
160 changes: 159 additions & 1 deletion src/FluentModbus/Client/ModbusRtuOverTcpClientAsync.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,168 @@

/* This is automatically translated code. */

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

namespace FluentModbus;

public partial class ModbusRtuOverTcpClient
{
/// <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. The default value is <see cref="CancellationToken.None"/>.</param>
public async Task ConnectAsync(CancellationToken cancellationToken = default)
{
await ConnectAsync(ModbusEndianness.LittleEndian, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Connect to localhost at port 502.
/// </summary>
/// <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. The default value is <see cref="CancellationToken.None"/>.</param>
public async Task ConnectAsync(ModbusEndianness endianness, CancellationToken cancellationToken = default)
{
await ConnectAsync(new IPEndPoint(IPAddress.Loopback, 502), endianness, cancellationToken).ConfigureAwait(false);
}

/// <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>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None"/>.</param>
public async Task ConnectAsync(string remoteEndpoint, CancellationToken cancellationToken = default)
{
await ConnectAsync(remoteEndpoint, ModbusEndianness.LittleEndian, cancellationToken).ConfigureAwait(false);
}

/// <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>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None"/>.</param>
public async 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
await ConnectAsync(parsedRemoteEndpoint!, endianness, cancellationToken).ConfigureAwait(false);
#endif

#if NETSTANDARD2_1_OR_GREATER
await ConnectAsync(parsedRemoteEndpoint, endianness, cancellationToken).ConfigureAwait(false);
#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>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None"/>.</param>
public async Task ConnectAsync(IPAddress remoteIpAddress, CancellationToken cancellationToken = default)
{
await ConnectAsync(remoteIpAddress, ModbusEndianness.LittleEndian, cancellationToken).ConfigureAwait(false);
}

/// <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>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None"/>.</param>
public async Task ConnectAsync(IPAddress remoteIpAddress, ModbusEndianness endianness, CancellationToken cancellationToken = default)
{
await ConnectAsync(new IPEndPoint(remoteIpAddress, 502), endianness, cancellationToken).ConfigureAwait(false);
}

/// <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>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None"/>.</param>
public async Task ConnectAsync(IPEndPoint remoteEndpoint, CancellationToken cancellationToken = default)
{
await ConnectAsync(remoteEndpoint, ModbusEndianness.LittleEndian, cancellationToken).ConfigureAwait(false);
}

/// <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>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None"/>.</param>
public async Task ConnectAsync(IPEndPoint remoteEndpoint, ModbusEndianness endianness, CancellationToken cancellationToken = default)
{
await InitializeAsync(new TcpClient(), remoteEndpoint, endianness, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// 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. The default value is <see cref="CancellationToken.None"/>.</param>
public async Task InitializeAsync(TcpClient tcpClient, ModbusEndianness endianness, CancellationToken cancellationToken = default)
{
await InitializeAsync(tcpClient, default, endianness, cancellationToken).ConfigureAwait(false);
}

///<inheritdoc/>
private async Task InitializeAsync(TcpClient tcpClient, IPEndPoint? remoteEndpoint, ModbusEndianness endianness, CancellationToken cancellationToken = default)
{
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)
{
using var timeoutCts = new CancellationTokenSource(ConnectTimeout);
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);

var connectTask = tcpClient.ConnectAsync(remoteEndpoint.Address, remoteEndpoint.Port);
var cancellationTask = Task.Delay(-1, linkedCts.Token);

var completedTask = await Task.WhenAny(connectTask, cancellationTask).ConfigureAwait(false);

if (completedTask == cancellationTask)
{
tcpClient.Close(); // Cancel the connect attempt

if (cancellationToken.IsCancellationRequested)
throw new OperationCanceledException(cancellationToken);

throw new Exception(ErrorMessage.ModbusClient_TcpConnectTimeout);
}

await connectTask.ConfigureAwait(false); // Surface any connection exceptions
}

// 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 @@ -117,5 +275,5 @@ protected override async Task<Memory<byte>> TransceiveFrameAsync(byte unitIdenti
throw new ModbusException(ErrorMessage.ModbusClient_InvalidResponseFunctionCode);

return _frameBuffer.Buffer.AsMemory(1, frameLength - 3);
}
}
}
60 changes: 48 additions & 12 deletions src/FluentModbus/Client/ModbusRtuOverTcpClientAsync.tt
Original file line number Diff line number Diff line change
@@ -1,29 +1,65 @@
<#@ template language="C#" #>
<#@ output extension=".cs" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Net" #>
<#@ import namespace="System.Text.RegularExpressions" #>

<#
var csstring = File.ReadAllText("src/FluentModbus/Client/ModbusRtuOverTcpClient.cs");
var match = Regex.Matches(csstring, @"(protected override Span<byte> TransceiveFrame\()(.*?)\)(.*?\n })", RegexOptions.Singleline)[0];
var methodsToConvert = Regex.Matches(csstring, @" \/{3}(?:.(?!\n\n))*?public void (?:Connect|Initialize)\(.*?\n }", RegexOptions.Singleline);
var matchPrivInit = Regex.Matches(csstring, @" (private void Initialize\()(.*?)\)(.*?\n })", RegexOptions.Singleline)[0];
var match = Regex.Matches(csstring, @" (protected override Span<byte> TransceiveFrame\()(.*?)\)(.*?\n })", RegexOptions.Singleline)[0];

#>
/* This is automatically translated code. */

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

namespace FluentModbus;

public partial class ModbusRtuOverTcpClient
{
<#
// 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}");
#>
<#
foreach (Match method in methodsToConvert)
{
var methodString = method.Value;

// add cancellation token XML comment
methodString = Regex.Replace(methodString, @"(>\r?\n) (public)", m => $"{m.Groups[1]} /// <param name=\"cancellationToken\">The token to monitor for cancellation requests. The default value is <see cref=\"CancellationToken.None\"/>.</param>\n {m.Groups[2]}", RegexOptions.Singleline);

// add cancellation token
methodString = Regex.Replace(methodString, @"(>\r?\n (?:public).*?)\((.*?)\)", m => $"{m.Groups[1]}({m.Groups[2]}{(m.Groups[2].Length > 0 ? ", " : "")}CancellationToken cancellationToken = default)");

// replace return values
methodString = Regex.Replace(methodString, " void", " async Task");

// replace method name
methodString = Regex.Replace(methodString, @"(.* Task[^ ]*) (.*?)([<|\(])", m => $"{m.Groups[1]} {m.Groups[2]}Async{m.Groups[3]}");

// update Connect and Initialize calls
methodString = Regex.Replace(methodString, @"(Connect|Initialize)\((.*?)\);", m => $"await {m.Groups[1]}Async({m.Groups[2]}, cancellationToken).ConfigureAwait(false);");

methodString += "\n\n";
Write(methodString);
}

var signaturePrivInit = matchPrivInit.Groups[2].Value;
var bodyPrivInit = matchPrivInit.Groups[3].Value;
bodyPrivInit = Regex.Replace(bodyPrivInit, @"if \(\!tcpClient.ConnectAsync\((.*?)\)\.Wait\((.*?)\)\)", m => $"var connectTask = tcpClient.ConnectAsync({m.Groups[1]});");
bodyPrivInit = Regex.Replace(bodyPrivInit, @"// ASYNC-ONLY: ", "");

Write($" ///<inheritdoc/>\n private async Task InitializeAsync({signaturePrivInit}, CancellationToken cancellationToken = default){bodyPrivInit}");
Write("\n\n");

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}");
#>

}
24 changes: 22 additions & 2 deletions src/FluentModbus/Client/ModbusTcpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,28 @@ private void Initialize(TcpClient tcpClient, IPEndPoint? remoteEndpoint, ModbusE
var isInternal = remoteEndpoint is not null;
_tcpClient = (tcpClient, isInternal);

if (remoteEndpoint is not null && !tcpClient.ConnectAsync(remoteEndpoint.Address, remoteEndpoint.Port).Wait(ConnectTimeout))
throw new Exception(ErrorMessage.ModbusClient_TcpConnectTimeout);
if (remoteEndpoint is not null)
{
// ASYNC-ONLY: using var timeoutCts = new CancellationTokenSource(ConnectTimeout);
// ASYNC-ONLY: using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
// ASYNC-ONLY:
if (!tcpClient.ConnectAsync(remoteEndpoint.Address, remoteEndpoint.Port).Wait(ConnectTimeout))
// ASYNC-ONLY: var cancellationTask = Task.Delay(-1, linkedCts.Token);
// ASYNC-ONLY:
// ASYNC-ONLY: var completedTask = await Task.WhenAny(connectTask, cancellationTask).ConfigureAwait(false);
// ASYNC-ONLY:
// ASYNC-ONLY: if (completedTask == cancellationTask)
// ASYNC-ONLY: {
// ASYNC-ONLY: tcpClient.Close(); // Cancel the connect attempt
// ASYNC-ONLY:
// ASYNC-ONLY: if (cancellationToken.IsCancellationRequested)
// ASYNC-ONLY: throw new OperationCanceledException(cancellationToken);
// ASYNC-ONLY:
throw new Exception(ErrorMessage.ModbusClient_TcpConnectTimeout);
// ASYNC-ONLY: }
// ASYNC-ONLY:
// ASYNC-ONLY: await connectTask.ConfigureAwait(false); // Surface any connection exceptions
}

// Why no method signature with NetworkStream only and then set the timeouts
// in the Connect method like for the RTU client?
Expand Down
Loading