Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
Apollo3zehn committed Sep 8, 2022
2 parents 23f579f + 2ef9a4e commit ce86dbd
Show file tree
Hide file tree
Showing 22 changed files with 271 additions and 85 deletions.
24 changes: 24 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
// Verwendet IntelliSense zum Ermitteln möglicher Attribute.
// Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
// Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (console)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/artifacts/bin/SampleServerClientTcp/net6.0/SampleServerClientTcp.dll",
"args": [],
"cwd": "${workspaceFolder}/sample/SampleServerClientTcp",
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
]
}
17 changes: 17 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/sample/SampleServerClientTcp/SampleServerClientTcp.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
}
]
}
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## v5.0.0 - 2022-09-08

### Breaking Changes
- The previously introduced TCP client constructor overload was called `Connect` although it expected a totally externally managed TCP client which should already be connected. This constructor is now named `Initialize` and its signature has been adapted to better fit its purpose. The passed TCP client (or `IModbusRtuSerialPort` in case of the RTU client) is now not modified at all, i.e. configured timeouts or other things are not applied to these externally managed instances (#78).

### Features
- Modbus TCP and RTU clients implement `IDisposable` so you can do the following now: `using var client = new ModbusTcpClient(...)` (#67)
- Modbus server base class has now a virtual `Stop` method so the actual server can be stopped using a base class reference (#79).

### Bugs Fixed
- The Modbus server ignored the unit identifier and responded to all requests (#79).
- Modbus server side read timeout exception handling is more defined now:
- The TCP server closes the connection.
- The Modbus RTU server ignores the exception as there is only a single connection and if that one is closed, there would be no point in keeping the RTU server running.
- Modbus server did not properly handle asynchronous cancellation (#79).

> [See API changes on Fuget.org](https://www.fuget.org/packages/FluentModbus/5.0.0/lib/netstandard2.1/diff/4.1.0/)
Thanks @schotime and @LukasKarel for your PRs!
3 changes: 3 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<RestoreAdditionalProjectSources>
https://www.myget.org/F/apollo3zehn-dev/api/v3/index.json
</RestoreAdditionalProjectSources>
</PropertyGroup>

<PropertyGroup>
Expand Down
27 changes: 20 additions & 7 deletions build/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,31 @@
if final_version in (release["name"] for release in releases):
raise Exception(f"Release {final_version} already exists.")

print(" unique release: OK")
print(" unique release: OK")

# prompt for annotation
print("Please enter the release message (type 'quit' to stop):")
lines = itertools.takewhile(lambda x: x.strip() != "quit" and x.strip() != "quit()", sys.stdin)
release_message = "".join(lines).rstrip('\n')
# get annotation
with open("CHANGELOG.md") as file:
changelog = file.read()

matches = list(re.finditer(r"^##\s(.*?)\s-\s[0-9]{4}-[0-9]{2}-[0-9]{2}(.*?)(?=(?:\Z|^##\s))", changelog, re.MULTILINE | re.DOTALL))

if not matches:
raise Exception(f"The file CHANGELOG.md is malformed.")

match_for_version = next((match for match in matches if match[1] == final_version), None)

if not match_for_version:
raise Exception(f"No change log entry found for version {final_version}.")

release_message = match_for_version[2].strip()

print("extract annotation: OK")

# create tag
subprocess.check_output(["git", "tag", "-a", final_version, "-m", release_message, "--cleanup=whitespace"], stdin=None, stderr=None, shell=False)

print(" create tag: OK")
print(" create tag: OK")

# push tag
subprocess.check_output(["git", "push", "--quiet", "origin", final_version], stdin=None, stderr=None, shell=False)
print(" push tag: OK")
print(" push tag: OK")
2 changes: 1 addition & 1 deletion doc/samples/modbus_rtu.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ When everything is prepared, first start the server ...
client.Close();

Console.WriteLine("Tests finished. Press any key to continue.");
Console.ReadKey(true);
Console.ReadKey(intercept: true);
});

```
Expand Down
2 changes: 1 addition & 1 deletion doc/samples/modbus_tcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ When everything is prepared, first start the server ...
client.Disconnect();

Console.WriteLine("Tests finished. Press any key to continue.");
Console.ReadKey(true);
Console.ReadKey(intercept: true);
});

```
Expand Down
7 changes: 3 additions & 4 deletions sample/SampleServerClientRtu/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,11 @@ static async Task Main(string[] args)

/* run Modbus RTU server */
var cts = new CancellationTokenSource();
server.Start(serverPort);
serverLogger.LogInformation("Server started.");

var task_server = Task.Run(async () =>
{
server.Start(serverPort);
serverLogger.LogInformation("Server started.");

while (!cts.IsCancellationRequested)
{
// lock is required to synchronize buffer access between this application and the Modbus client
Expand Down Expand Up @@ -86,7 +85,7 @@ static async Task Main(string[] args)
client.Close();

Console.WriteLine("Tests finished. Press any key to continue.");
Console.ReadKey(true);
Console.ReadKey(intercept: true);
});

// wait for client task to finish
Expand Down
7 changes: 3 additions & 4 deletions sample/SampleServerClientTcp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,11 @@ static async Task Main(string[] args)

/* run Modbus TCP server */
var cts = new CancellationTokenSource();
server.Start();
serverLogger.LogInformation("Server started.");

var task_server = Task.Run(async () =>
{
server.Start();
serverLogger.LogInformation("Server started.");

while (!cts.IsCancellationRequested)
{
// lock is required to synchronize buffer access between this application and one or more Modbus clients
Expand Down Expand Up @@ -70,7 +69,7 @@ static async Task Main(string[] args)
client.Disconnect();

Console.WriteLine("Tests finished. Press any key to continue.");
Console.ReadKey(true);
Console.ReadKey(intercept: true);
});

// wait for client task to finish
Expand Down
File renamed without changes.
46 changes: 31 additions & 15 deletions src/FluentModbus/Client/ModbusRtuClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace FluentModbus
/// <summary>
/// A Modbus RTU client.
/// </summary>
public partial class ModbusRtuClient : ModbusClient
public partial class ModbusRtuClient : ModbusClient, IDisposable
{
#region Field

Expand Down Expand Up @@ -95,29 +95,20 @@ public void Connect(string port, ModbusEndianness endianness)
WriteTimeout = WriteTimeout
});

Connect(serialPort, isInternal: true, endianness);
Initialize(serialPort, isInternal: true, endianness);
}

/// <summary>
/// Connect to the specified <paramref name="serialPort"/>.
/// </summary>
/// <param name="serialPort">The externally managed <see cref="ModbusRtuSerialPort"/>.</param>
public void Connect(IModbusRtuSerialPort serialPort)
{
Connect(serialPort, isInternal: false, ModbusEndianness.LittleEndian);
}

/// <summary>
/// Connect to the specified <paramref name="serialPort"/>.
/// Initialize the Modbus TCP client with an externally managed <see cref="IModbusRtuSerialPort"/>.
/// </summary>
/// <param name="serialPort">The externally managed <see cref="IModbusRtuSerialPort"/>.</param>
/// <param name="endianness">Specifies the endianness of the data exchanged with the Modbus server.</param>
public void Connect(IModbusRtuSerialPort serialPort, ModbusEndianness endianness)
public void Initialize(IModbusRtuSerialPort serialPort, ModbusEndianness endianness)
{
Connect(serialPort, isInternal: false, endianness);
Initialize(serialPort, isInternal: false, endianness);
}

private void Connect(IModbusRtuSerialPort serialPort, bool isInternal, ModbusEndianness endianness)
private void Initialize(IModbusRtuSerialPort serialPort, bool isInternal, ModbusEndianness endianness)
{
/* According to the spec (https://www.modbus.org/docs/Modbus_over_serial_line_V1_02.pdf),
* section 2.5.1 RTU Transmission Mode: "... the use of no parity requires 2 stop bits."
Expand Down Expand Up @@ -234,5 +225,30 @@ protected override Span<byte> TransceiveFrame(byte unitIdentifier, ModbusFunctio
}

#endregion

#region IDisposable

private bool _disposedValue;

protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
Close();
}

_disposedValue = true;
}
}

public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

#endregion
}
}
56 changes: 46 additions & 10 deletions src/FluentModbus/Client/ModbusTcpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace FluentModbus
/// <summary>
/// A Modbus TCP client.
/// </summary>
public partial class ModbusTcpClient : ModbusClient
public partial class ModbusTcpClient : ModbusClient, IDisposable
{
#region Fields

Expand Down Expand Up @@ -141,21 +141,20 @@ public void Connect(IPEndPoint remoteEndpoint)
/// <param name="endianness">Specifies the endianness of the data exchanged with the Modbus server.</param>
public void Connect(IPEndPoint remoteEndpoint, ModbusEndianness endianness)
{
Connect(new TcpClient(), isInternal: true, remoteEndpoint, endianness);
Initialize(new TcpClient(), remoteEndpoint, endianness);
}

/// <summary>
/// Connect to the specified <paramref name="remoteEndpoint"/>.
/// 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="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 void Connect(TcpClient tcpClient, IPEndPoint remoteEndpoint, ModbusEndianness endianness)
public void Initialize(TcpClient tcpClient, ModbusEndianness endianness)
{
Connect(tcpClient, isInternal: false, remoteEndpoint, endianness);
Initialize(tcpClient, default, endianness);
}

private void Connect(TcpClient tcpClient, bool isInternal, IPEndPoint remoteEndpoint, ModbusEndianness endianness)
private void Initialize(TcpClient tcpClient, IPEndPoint? remoteEndpoint, ModbusEndianness endianness)
{
base.SwapBytes = BitConverter.IsLittleEndian && endianness == ModbusEndianness.BigEndian ||
!BitConverter.IsLittleEndian && endianness == ModbusEndianness.LittleEndian;
Expand All @@ -165,14 +164,26 @@ private void Connect(TcpClient tcpClient, bool isInternal, IPEndPoint remoteEndp
if (_tcpClient.HasValue && _tcpClient.Value.IsInternal)
_tcpClient.Value.Value.Close();

var isInternal = remoteEndpoint is not null;
_tcpClient = (tcpClient, isInternal);

if (!tcpClient.ConnectAsync(remoteEndpoint.Address, remoteEndpoint.Port).Wait(ConnectTimeout))
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
// 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();
_networkStream.ReadTimeout = ReadTimeout;
_networkStream.WriteTimeout = WriteTimeout;

if (isInternal)
{
_networkStream.ReadTimeout = ReadTimeout;
_networkStream.WriteTimeout = WriteTimeout;
}
}

/// <summary>
Expand Down Expand Up @@ -328,5 +339,30 @@ private ushort GetTransactionIdentifier()
}

#endregion

#region IDisposable

private bool _disposedValue;

protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
Disconnect();
}

_disposedValue = true;
}
}

public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

#endregion
}
}
6 changes: 5 additions & 1 deletion src/FluentModbus/ModbusRtuSerialPort.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ public async Task<int> ReadAsync(byte[] buffer, int offset, int count, Cancellat
using var timeoutCts = new CancellationTokenSource(_serialPort.ReadTimeout);

/* _serialPort.DiscardInBuffer is essential here to cancel the operation */
using (timeoutCts.Token.Register(() => _serialPort.DiscardInBuffer()))
using (timeoutCts.Token.Register(() =>
{
if (IsOpen)
_serialPort.DiscardInBuffer();
}))
using (token.Register(() => timeoutCts.Cancel()))
{
try
Expand Down
Loading

0 comments on commit ce86dbd

Please sign in to comment.