├── version.json
├── doc
├── dark-mode
│ └── public
│ │ └── main.js
├── toc.yml
├── images
│ ├── icon.ico
│ ├── icon.png
│ ├── sample.png
│ └── logo.svg
├── filterConfig.yml
├── samples
│ ├── toc.yml
│ ├── modbus_validator.md
│ ├── modbus_tcp.md
│ └── modbus_rtu.md
└── docfx.json
├── .vscode
├── settings.json
├── tasks.json
└── launch.json
├── .gitignore
├── src
├── FluentModbus
│ ├── IsExternalInit.cs
│ ├── Server
│ │ ├── ITcpClientProvider.cs
│ │ ├── DefaultTcpClientProvider.cs
│ │ ├── ModbusRtuRequestHandler.cs
│ │ ├── ModbusTcpRequestHandler.cs
│ │ ├── ModbusRtuServer.cs
│ │ └── ModbusTcpServer.cs
│ ├── ModbusEndianness.cs
│ ├── ModbusException.cs
│ ├── CastMemoryManager.cs
│ ├── ModbusFrameBuffer.cs
│ ├── Client
│ │ ├── ModbusRtuClientAsync.tt
│ │ ├── ModbusRtuOverTcpClientAsync.tt
│ │ ├── ModbusTcpClientAsync.tt
│ │ ├── ModbusClientAsync.tt
│ │ ├── ModbusRtuClientAsync.cs
│ │ ├── ModbusRtuOverTcpClientAsync.cs
│ │ ├── ModbusTcpClientAsync.cs
│ │ └── ModbusRtuClient.cs
│ ├── ModbusExceptionCode.cs
│ ├── ExtendedBinaryReader.cs
│ ├── ExtendedBinaryWriter.cs
│ ├── ModbusFunctionCode.cs
│ ├── IModbusRtuSerialPort.cs
│ ├── FluentModbus.csproj
│ ├── ModbusRtuSerialPort.cs
│ ├── ModbusUtils.cs
│ ├── SpanExtensions.cs
│ └── Resources
│ │ └── ErrorMessage.resx
└── Directory.Build.props
├── tests
├── Directory.Build.props
└── FluentModbus.Tests
│ ├── Support
│ ├── XUnitFixture.cs
│ ├── EndpointSource.cs
│ └── FakeSerialPort.cs
│ ├── ModbusTcpClientTests.cs
│ ├── FluentModbus.Tests.csproj
│ ├── ModbusRtuServerTests.cs
│ ├── ModbusUtilsTests.cs
│ ├── ModbusTcpServerTests.cs
│ └── ProtocolTestsAsync.cs
├── solution.json
├── sample
├── SampleServerClientRtu
│ ├── SampleServerClientRtu.csproj
│ └── Program.cs
└── SampleServerClientTcp
│ ├── SampleServerClientTcp.csproj
│ └── Program.cs
├── Directory.Build.props
├── LICENSE.md
├── README.md
├── .editorconfig
├── CHANGELOG.md
├── .github
└── workflows
│ └── build-and-publish.yml
└── FluentModbus.sln
/version.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "5.2.0",
3 | "suffix": ""
4 | }
--------------------------------------------------------------------------------
/doc/dark-mode/public/main.js:
--------------------------------------------------------------------------------
1 | export default {
2 | defaultTheme: 'dark'
3 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "dotnet.defaultSolution": "FluentModbus.sln"
3 | }
--------------------------------------------------------------------------------
/doc/toc.yml:
--------------------------------------------------------------------------------
1 | - name: Samples
2 | href: samples/
3 |
4 | - name: API
5 | href: api/
6 |
--------------------------------------------------------------------------------
/doc/images/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Apollo3zehn/FluentModbus/HEAD/doc/images/icon.ico
--------------------------------------------------------------------------------
/doc/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Apollo3zehn/FluentModbus/HEAD/doc/images/icon.png
--------------------------------------------------------------------------------
/doc/images/sample.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Apollo3zehn/FluentModbus/HEAD/doc/images/sample.png
--------------------------------------------------------------------------------
/doc/filterConfig.yml:
--------------------------------------------------------------------------------
1 | apiRules:
2 |
3 | - exclude:
4 | uidRegex: ^System\.Object
5 | type: Type
--------------------------------------------------------------------------------
/doc/samples/toc.yml:
--------------------------------------------------------------------------------
1 | - name: Modbus TCP
2 | href: modbus_tcp.md
3 |
4 | - name: Modbus RTU
5 | href: modbus_rtu.md
6 |
7 | - name: Modbus Validator
8 | href: modbus_validator.md
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .venv/
2 | .vs/
3 | artifacts/
4 | BenchmarkDotNet.Artifacts
5 |
6 | doc/_site
7 | /**/obj
8 | **/wwwroot/lib/
9 |
10 | doc/api/*
11 |
12 | *.suo
13 | *.user
14 | *.pyc
--------------------------------------------------------------------------------
/src/FluentModbus/IsExternalInit.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 |
3 | // workaround class for records and "init" keyword
4 | namespace System.Runtime.CompilerServices;
5 |
6 | [EditorBrowsable(EditorBrowsableState.Never)]
7 | internal class IsExternalInit { }
--------------------------------------------------------------------------------
/tests/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | VSTHRD200
7 |
8 |
9 |
--------------------------------------------------------------------------------
/solution.json:
--------------------------------------------------------------------------------
1 | {
2 | "authors": "https://github.com/Apollo3zehn",
3 | "company": "https://github.com/Apollo3zehn",
4 | "copyright": "Apollo3zehn",
5 | "product": "FluentModbus",
6 | "license": "MIT",
7 | "project-url": "https://apollo3zehn.github.io/FluentModbus",
8 | "repository-url": "https://github.com/Apollo3zehn/FluentModbus"
9 | }
--------------------------------------------------------------------------------
/src/FluentModbus/Server/ITcpClientProvider.cs:
--------------------------------------------------------------------------------
1 | using System.Net.Sockets;
2 |
3 | namespace FluentModbus;
4 |
5 | ///
6 | /// Provides TCP clients.
7 | ///
8 | public interface ITcpClientProvider : IDisposable
9 | {
10 | ///
11 | /// Accepts the next TCP client.
12 | ///
13 | ///
14 | Task AcceptTcpClientAsync();
15 | }
16 |
--------------------------------------------------------------------------------
/tests/FluentModbus.Tests/Support/XUnitFixture.cs:
--------------------------------------------------------------------------------
1 | namespace FluentModbus;
2 |
3 | public class XUnitFixture
4 | {
5 | public XUnitFixture()
6 | {
7 | ModbusTcpServer.DefaultConnectionTimeout = TimeSpan.FromSeconds(10);
8 | ModbusTcpClient.DefaultConnectTimeout = (int)TimeSpan.FromSeconds(10).TotalMilliseconds;
9 | }
10 |
11 | public void Dispose()
12 | {
13 | //
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/FluentModbus/ModbusEndianness.cs:
--------------------------------------------------------------------------------
1 | namespace FluentModbus;
2 |
3 | ///
4 | /// Specifies the endianness of the data.
5 | ///
6 | public enum ModbusEndianness
7 | {
8 | ///
9 | /// Little endian data layout, i.e. the least significant byte is trasmitted first.
10 | ///
11 | LittleEndian = 1,
12 |
13 | ///
14 | /// Big endian data layout, i.e. the most significant byte is trasmitted first.
15 | ///
16 | BigEndian = 2,
17 | }
18 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build",
6 | "command": "dotnet",
7 | "type": "process",
8 | "args": [
9 | "build",
10 | "${workspaceFolder}/sample/SampleServerClientTcp/SampleServerClientTcp.csproj",
11 | "/property:GenerateFullPaths=true",
12 | "/consoleloggerparameters:NoSummary"
13 | ],
14 | "problemMatcher": "$msCompile"
15 | }
16 | ]
17 | }
--------------------------------------------------------------------------------
/sample/SampleServerClientRtu/SampleServerClientRtu.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | false
5 | Exe
6 | $(TargetFrameworkVersion)
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/sample/SampleServerClientTcp/SampleServerClientTcp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | false
5 | Exe
6 | $(TargetFrameworkVersion)
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/FluentModbus.Tests/Support/EndpointSource.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 |
3 | namespace FluentModbus.Tests;
4 |
5 | public static class EndpointSource
6 | {
7 | private static int _current = 10000;
8 | private static object _lock = new object();
9 |
10 | public static IPEndPoint GetNext()
11 | {
12 | lock (_lock)
13 | {
14 | if (_current == 65535)
15 | {
16 | throw new NotSupportedException("There are no more free ports available.");
17 | }
18 |
19 | return new IPEndPoint(IPAddress.Loopback, _current++);
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | true
5 | net8.0
6 | enable
7 | enable
8 | true
9 | latest
10 |
11 | https://www.myget.org/F/apollo3zehn-dev/api/v3/index.json
12 |
13 | true
14 | $(MSBuildThisFileDirectory)artifacts
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/FluentModbus/ModbusException.cs:
--------------------------------------------------------------------------------
1 | namespace FluentModbus;
2 |
3 | ///
4 | /// This exception is used for Modbus protocol errors.
5 | ///
6 | public class ModbusException : Exception
7 | {
8 | internal ModbusException(string message) : base(message)
9 | {
10 | ExceptionCode = (ModbusExceptionCode)255;
11 | }
12 |
13 | internal ModbusException(ModbusExceptionCode exceptionCode, string message) : base(message)
14 | {
15 | ExceptionCode = exceptionCode;
16 | }
17 |
18 | ///
19 | /// The Modbus exception code. A value of 255 indicates that there is no specific exception code.
20 | ///
21 | public ModbusExceptionCode ExceptionCode { get; }
22 | }
23 |
--------------------------------------------------------------------------------
/src/FluentModbus/CastMemoryManager.cs:
--------------------------------------------------------------------------------
1 | using System.Buffers;
2 | using System.Runtime.InteropServices;
3 |
4 | namespace FluentModbus;
5 |
6 | internal class CastMemoryManager : MemoryManager
7 | where TFrom : struct
8 | where TTo : struct
9 | {
10 | private readonly Memory _from;
11 |
12 | public CastMemoryManager(Memory from) => _from = from;
13 |
14 | public override Span GetSpan() => MemoryMarshal.Cast(_from.Span);
15 |
16 | protected override void Dispose(bool disposing)
17 | {
18 | //
19 | }
20 |
21 | public override MemoryHandle Pin(int elementIndex = 0) => throw new NotSupportedException();
22 |
23 | public override void Unpin() => throw new NotSupportedException();
24 | }
25 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Verwendet IntelliSense zum Ermitteln möglicher Attribute.
3 | // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
4 | // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Sample: TCP",
9 | "type": "coreclr",
10 | "request": "launch",
11 | "preLaunchTask": "build",
12 | "program": "${workspaceFolder}/artifacts/bin/SampleServerClientTcp/debug/SampleServerClientTcp.dll",
13 | "args": [],
14 | "cwd": "${workspaceFolder}/sample/SampleServerClientTcp",
15 | "console": "externalTerminal",
16 | "stopAtEntry": false
17 | }
18 | ]
19 | }
--------------------------------------------------------------------------------
/src/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | true
8 | embedded
9 | true
10 | true
11 |
12 |
13 |
14 | $(VERSION)+$(GITHUB_SHA)
15 | $(VERSION.Split('.')[0]).0.0.0
16 | $(VERSION.Split('.')[0]).$(VERSION.Split('.')[1]).$(VERSION.Split('.')[2].Split('-')[0]).0
17 |
18 |
19 |
--------------------------------------------------------------------------------
/tests/FluentModbus.Tests/ModbusTcpClientTests.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using Xunit;
3 |
4 | namespace FluentModbus.Tests;
5 |
6 | public class ModbusTcpClientTests : IClassFixture
7 | {
8 | [Fact]
9 | public void ClientRespectsConnectTimeout()
10 | {
11 | // Arrange
12 | var endpoint = EndpointSource.GetNext();
13 | var connectTimeout = 500;
14 |
15 | var client = new ModbusTcpClient()
16 | {
17 | ConnectTimeout = connectTimeout
18 | };
19 |
20 | // Act
21 | var sw = Stopwatch.StartNew();
22 |
23 | try
24 | {
25 | client.Connect(endpoint);
26 | }
27 | catch (Exception)
28 | {
29 | // Assert
30 | var elapsed = sw.ElapsedMilliseconds;
31 |
32 | Assert.True(elapsed < connectTimeout * 2, "The connect timeout is not respected.");
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/tests/FluentModbus.Tests/FluentModbus.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | false
5 | $(TargetFrameworkVersion)
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | all
15 | runtime; build; native; contentfiles; analyzers
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [2022] [Apollo3zehn]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/FluentModbus/Server/DefaultTcpClientProvider.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Net.Sockets;
3 |
4 | namespace FluentModbus;
5 |
6 | internal class DefaultTcpClientProvider : ITcpClientProvider
7 | {
8 | #region Fields
9 |
10 | private TcpListener _tcpListener;
11 |
12 | #endregion
13 |
14 | #region Constructors
15 |
16 | public DefaultTcpClientProvider(IPEndPoint endPoint)
17 | {
18 | _tcpListener = new TcpListener(endPoint);
19 | _tcpListener.Start();
20 | }
21 |
22 | #endregion
23 |
24 | #region Methods
25 |
26 | public Task AcceptTcpClientAsync()
27 | {
28 | return _tcpListener.AcceptTcpClientAsync();
29 | }
30 |
31 | #endregion
32 |
33 | #region IDisposable Support
34 |
35 | private bool _disposedValue;
36 |
37 | protected virtual void Dispose(bool disposing)
38 | {
39 | if (!_disposedValue)
40 | {
41 | if (disposing)
42 | {
43 | _tcpListener.Stop();
44 | }
45 |
46 | _disposedValue = true;
47 | }
48 | }
49 |
50 | public void Dispose()
51 | {
52 | Dispose(disposing: true);
53 | GC.SuppressFinalize(this);
54 | }
55 |
56 | #endregion
57 | }
58 |
--------------------------------------------------------------------------------
/src/FluentModbus/ModbusFrameBuffer.cs:
--------------------------------------------------------------------------------
1 | using System.Buffers;
2 |
3 | namespace FluentModbus;
4 |
5 | internal class ModbusFrameBuffer : IDisposable
6 | {
7 | #region Constructors
8 |
9 | public ModbusFrameBuffer(int size)
10 | {
11 | Buffer = ArrayPool.Shared.Rent(size);
12 |
13 | Writer = new ExtendedBinaryWriter(new MemoryStream(Buffer));
14 | Reader = new ExtendedBinaryReader(new MemoryStream(Buffer));
15 | }
16 |
17 | #endregion
18 |
19 | #region Properties
20 |
21 | public byte[] Buffer { get; }
22 |
23 | public ExtendedBinaryWriter Writer { get; }
24 | public ExtendedBinaryReader Reader { get; }
25 |
26 | #endregion
27 |
28 | #region IDisposable Support
29 |
30 | private bool _disposedValue = false;
31 |
32 | protected virtual void Dispose(bool disposing)
33 | {
34 | if (!_disposedValue)
35 | {
36 | if (disposing)
37 | {
38 | Writer.Dispose();
39 | Reader.Dispose();
40 |
41 | ArrayPool.Shared.Return(Buffer);
42 | }
43 |
44 | _disposedValue = true;
45 | }
46 | }
47 |
48 | public void Dispose()
49 | {
50 | Dispose(true);
51 | }
52 |
53 | #endregion
54 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FluentModbus
2 |
3 | [](https://github.com/Apollo3zehn/FluentModbus/actions) [](https://www.nuget.org/packages/FluentModbus)
4 |
5 | FluentModbus is a .NET Standard library (2.0 and 2.1) that provides Modbus TCP/RTU server and client implementations for easy process data exchange. Both, the server and the client, implement class 0, class 1 and class 2 (partially) functions of the [specification](http://www.modbus.org/specs.php). Namely, these are:
6 |
7 | #### Class 0:
8 | * FC03: ReadHoldingRegisters
9 | * FC16: WriteMultipleRegisters
10 |
11 | #### Class 1:
12 | * FC01: ReadCoils
13 | * FC02: ReadDiscreteInputs
14 | * FC04: ReadInputRegisters
15 | * FC05: WriteSingleCoil
16 | * FC06: WriteSingleRegister
17 |
18 | #### Class 2:
19 | * FC15: WriteMultipleCoils
20 | * FC23: ReadWriteMultipleRegisters
21 |
22 | Please see the [introduction](https://apollo3zehn.github.io/FluentModbus/) to get a more detailed description on how to use this library!
23 |
24 | Below is a screenshot of the [sample](https://apollo3zehn.github.io/FluentModbus/samples/modbus_tcp.html) console output using a Modbus TCP server and client:
25 |
26 | 
--------------------------------------------------------------------------------
/src/FluentModbus/Client/ModbusRtuClientAsync.tt:
--------------------------------------------------------------------------------
1 | <#@ template language="C#" #>
2 | <#@ output extension=".cs" #>
3 | <#@ import namespace="System.IO" #>
4 | <#@ import namespace="System.Text.RegularExpressions" #>
5 |
6 | <#
7 | var csstring = File.ReadAllText("src/FluentModbus/Client/ModbusRtuClient.cs");
8 | var match = Regex.Matches(csstring, @"(protected override Span TransceiveFrame\()(.*?)\)(.*?\n })", RegexOptions.Singleline)[0];
9 |
10 | #>
11 | /* This is automatically translated code. */
12 |
13 | namespace FluentModbus;
14 |
15 | public partial class ModbusRtuClient
16 | {
17 | <#
18 | // replace AsSpan
19 | var signature = match.Groups[2].Value;
20 | var body = match.Groups[3].Value;
21 | body = Regex.Replace(body, "AsSpan", "AsMemory");
22 | body = Regex.Replace(body, @"_serialPort!.Value.Value.Write\((.*?)\)", m => $"await _serialPort!.Value.Value.WriteAsync({m.Groups[1]}, cancellationToken).ConfigureAwait(false)");
23 | body = Regex.Replace(body, @"_serialPort!.Value.Value.Read\((.*?)\)", m => $"await _serialPort!.Value.Value.ReadAsync({m.Groups[1]}, cancellationToken).ConfigureAwait(false)");
24 |
25 | Write($"///\n protected override async Task> TransceiveFrameAsync({signature}, CancellationToken cancellationToken = default){body}");
26 | #>
27 |
28 | }
--------------------------------------------------------------------------------
/src/FluentModbus/Client/ModbusRtuOverTcpClientAsync.tt:
--------------------------------------------------------------------------------
1 | <#@ template language="C#" #>
2 | <#@ output extension=".cs" #>
3 | <#@ import namespace="System.IO" #>
4 | <#@ import namespace="System.Text.RegularExpressions" #>
5 |
6 | <#
7 | var csstring = File.ReadAllText("src/FluentModbus/Client/ModbusRtuOverTcpClient.cs");
8 | var match = Regex.Matches(csstring, @"(protected override Span TransceiveFrame\()(.*?)\)(.*?\n })", RegexOptions.Singleline)[0];
9 |
10 | #>
11 | /* This is automatically translated code. */
12 |
13 | namespace FluentModbus;
14 |
15 | public partial class ModbusRtuOverTcpClient
16 | {
17 | <#
18 | // replace AsSpan
19 | var signature = match.Groups[2].Value;
20 | var body = match.Groups[3].Value;
21 | body = Regex.Replace(body, "AsSpan", "AsMemory");
22 | body = Regex.Replace(body, @"_networkStream.Write\((.*?)\)", m => $"await _networkStream.WriteAsync({m.Groups[1]}, cancellationToken).ConfigureAwait(false)");
23 | body = Regex.Replace(body, @"_networkStream.Read\((.*?)\)", m => $"await _networkStream.ReadAsync({m.Groups[1]}, cancellationToken).ConfigureAwait(false)");
24 | body = Regex.Replace(body, @"// ASYNC-ONLY: ", "");
25 |
26 | Write($"///\n protected override async Task> TransceiveFrameAsync({signature}, CancellationToken cancellationToken = default){body}");
27 | #>
28 |
29 | }
--------------------------------------------------------------------------------
/src/FluentModbus/Client/ModbusTcpClientAsync.tt:
--------------------------------------------------------------------------------
1 | <#@ template language="C#" #>
2 | <#@ output extension=".cs" #>
3 | <#@ import namespace="System.IO" #>
4 | <#@ import namespace="System.Text.RegularExpressions" #>
5 |
6 | <#
7 | var csstring = File.ReadAllText("src/FluentModbus/Client/ModbusTcpClient.cs");
8 | var match = Regex.Matches(csstring, @"(protected override Span TransceiveFrame\()(.*?)\)(.*?\n })", RegexOptions.Singleline)[0];
9 |
10 | #>
11 | /* This is automatically translated code. */
12 |
13 | namespace FluentModbus
14 | {
15 | public partial class ModbusTcpClient
16 | {
17 | <#
18 | // replace AsSpan
19 | var signature = match.Groups[2].Value;
20 | var body = match.Groups[3].Value;
21 | body = Regex.Replace(body, "AsSpan", "AsMemory");
22 | body = Regex.Replace(body, @"_networkStream.Write\((.*?)\)", m => $"await _networkStream.WriteAsync({m.Groups[1]}, cancellationToken).ConfigureAwait(false)");
23 | body = Regex.Replace(body, @"_networkStream.Read\((.*?)\)", m => $"await _networkStream.ReadAsync({m.Groups[1]}, cancellationToken).ConfigureAwait(false)");
24 | body = Regex.Replace(body, @"// ASYNC-ONLY: ", "");
25 |
26 | Write($"///\n protected override async Task> TransceiveFrameAsync({signature}, CancellationToken cancellationToken = default){body}");
27 | #>
28 |
29 | }
30 | }
--------------------------------------------------------------------------------
/tests/FluentModbus.Tests/ModbusRtuServerTests.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using Xunit;
3 | using Xunit.Abstractions;
4 |
5 | namespace FluentModbus.Tests;
6 |
7 | public class ModbusRtuServerTests : IClassFixture
8 | {
9 | private ITestOutputHelper _logger;
10 |
11 | public ModbusRtuServerTests(ITestOutputHelper logger)
12 | {
13 | _logger = logger;
14 | }
15 |
16 | [Fact]
17 | public async void ServerHandlesRequestFire()
18 | {
19 | // Arrange
20 | var serialPort = new FakeSerialPort();
21 |
22 | using var server = new ModbusRtuServer(unitIdentifier: 1);
23 | server.Start(serialPort);
24 |
25 | var client = new ModbusRtuClient();
26 | client.Initialize(serialPort, ModbusEndianness.LittleEndian);
27 |
28 | await Task.Run(() =>
29 | {
30 | var data = Enumerable.Range(0, 20).Select(i => (float)i).ToArray();
31 | var sw = Stopwatch.StartNew();
32 | var iterations = 10000;
33 |
34 | for (int i = 0; i < iterations; i++)
35 | {
36 | client.WriteMultipleRegisters(0, 0, data);
37 | }
38 |
39 | var timePerRequest = sw.Elapsed.TotalMilliseconds / iterations;
40 | _logger.WriteLine($"Time per request: {timePerRequest * 1000:F0} us. Frequency: {1 / timePerRequest * 1000:F0} requests per second.");
41 |
42 | client.Close();
43 | });
44 |
45 | // Assert
46 | }
47 | }
--------------------------------------------------------------------------------
/doc/docfx.json:
--------------------------------------------------------------------------------
1 | {
2 | "metadata": [
3 | {
4 | "src": [
5 | {
6 | "files": [
7 | "**/*.csproj"
8 | ],
9 | "src": "../src"
10 | }
11 | ],
12 | "dest": "api",
13 | "filter": "filterConfig.yml",
14 | "properties": {
15 | "TargetFramework": "netstandard2.1"
16 | },
17 | "includePrivateMembers": false,
18 | "disableGitFeatures": false,
19 | "disableDefaultFilter": false,
20 | "noRestore": false,
21 | "namespaceLayout": "flattened",
22 | "memberLayout": "samePage",
23 | "EnumSortOrder": "alphabetic",
24 | "allowCompilationErrors": false
25 | }
26 | ],
27 | "build": {
28 | "content": [
29 | {
30 | "files": [
31 | "api/**.yml",
32 | "api/index.md"
33 | ]
34 | },
35 | {
36 | "files": [
37 | "samples/**.md",
38 | "samples/**/toc.yml",
39 | "toc.yml",
40 | "*.md"
41 | ]
42 | }
43 | ],
44 | "resource": [
45 | {
46 | "files": [
47 | "images/**"
48 | ]
49 | }
50 | ],
51 | "output": "_site",
52 | "globalMetadata": {
53 | "_appTitle": "FluentModbus",
54 | "_appFooter": "Copyright © 2024 Vincent Wilms",
55 | "_appFaviconPath": "images/icon.png",
56 | "_appLogoPath": "images/logo.svg"
57 | },
58 | "fileMetadataFiles": [],
59 | "template": [
60 | "default",
61 | "modern",
62 | "dark-mode"
63 | ],
64 | "postProcessors": [],
65 | "keepFileLink": false,
66 | "disableGitFeatures": false
67 | }
68 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # How to format:
2 | # (1) Add dotnet_diagnostic.XXXX.severity = error
3 | # (2) Run dotnet-format: dotnet format --diagnostics XXXX
4 |
5 | [*.cs]
6 | # "run cleanup": https://betterprogramming.pub/enforce-net-code-style-with-editorconfig-d2f0d79091ac
7 | # TODO: build real editorconfig file: https://github.com/dotnet/roslyn/blob/main/.editorconfig
8 |
9 | csharp_style_namespace_declarations = file_scoped:error
10 |
11 | # Enable naming rule violation errors on build (alternative: dotnet_analyzer_diagnostic.category-Style.severity = error)
12 | dotnet_diagnostic.IDE1006.severity = error
13 |
14 | #########################
15 | # example: https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/naming-rules#example-private-instance-fields-with-underscore
16 | #########################
17 |
18 | # Define the 'private_fields' symbol group:
19 | dotnet_naming_symbols.private_fields.applicable_kinds = field
20 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private
21 |
22 | # Define the 'private_static_fields' symbol group
23 | dotnet_naming_symbols.private_static_fields.applicable_kinds = field
24 | dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private
25 | dotnet_naming_symbols.private_static_fields.required_modifiers = static
26 |
27 | # Define the 'underscored' naming style
28 | dotnet_naming_style.underscored.capitalization = camel_case
29 | dotnet_naming_style.underscored.required_prefix = _
30 |
31 | # Define the 'private_fields_underscored' naming rule
32 | dotnet_naming_rule.private_fields_underscored.symbols = private_fields
33 | dotnet_naming_rule.private_fields_underscored.style = underscored
34 | dotnet_naming_rule.private_fields_underscored.severity = error
35 |
36 | # Define the 'private_static_fields_none' naming rule
37 | dotnet_naming_rule.private_static_fields_none.symbols = private_static_fields
38 | dotnet_naming_rule.private_static_fields_none.style = underscored
39 | dotnet_naming_rule.private_static_fields_none.severity = none
--------------------------------------------------------------------------------
/tests/FluentModbus.Tests/Support/FakeSerialPort.cs:
--------------------------------------------------------------------------------
1 | namespace FluentModbus;
2 |
3 | public class FakeSerialPort : IModbusRtuSerialPort
4 | {
5 | #region Fields
6 |
7 | private int _length;
8 | private byte[] _buffer;
9 | private AutoResetEvent _autoResetEvent;
10 |
11 | #endregion
12 |
13 | #region Constructors
14 |
15 | public FakeSerialPort()
16 | {
17 | _buffer = new byte[260];
18 | _autoResetEvent = new AutoResetEvent(false);
19 | }
20 |
21 | #endregion
22 |
23 | #region Properties
24 |
25 | public string PortName => "fake port";
26 |
27 | public bool IsOpen { get; set; }
28 |
29 | #endregion
30 |
31 | #region Methods
32 |
33 | public void Open()
34 | {
35 | IsOpen = true;
36 | }
37 |
38 | public void Close()
39 | {
40 | IsOpen = false;
41 | }
42 |
43 | public int Read(byte[] buffer, int offset, int count)
44 | {
45 | _autoResetEvent.WaitOne();
46 | Buffer.BlockCopy(_buffer, 0, buffer, offset, count);
47 |
48 | return _length;
49 | }
50 |
51 | public async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken token)
52 | {
53 | if (!IsOpen)
54 | throw new Exception("This method is only available when the port is open.");
55 |
56 | var registration = token.Register(() =>
57 | {
58 | _length = 0;
59 | _autoResetEvent.Set();
60 | });
61 |
62 | await Task.Run(() => Read(buffer, offset, count), token);
63 |
64 | registration.Dispose();
65 |
66 | if (_length == 0)
67 | throw new TaskCanceledException();
68 |
69 | return _length;
70 | }
71 |
72 | public void Write(byte[] buffer, int offset, int count)
73 | {
74 | Buffer.BlockCopy(buffer, offset, _buffer, 0, count);
75 |
76 | _length = count;
77 | _autoResetEvent.Set();
78 | }
79 |
80 | public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken token)
81 | {
82 | throw new NotImplementedException();
83 | }
84 |
85 | #endregion
86 | }
87 |
--------------------------------------------------------------------------------
/src/FluentModbus/ModbusExceptionCode.cs:
--------------------------------------------------------------------------------
1 | namespace FluentModbus;
2 |
3 | ///
4 | /// Specifies the Modbus exception type.
5 | ///
6 | public enum ModbusExceptionCode : byte
7 | {
8 | ///
9 | /// Only used by the server to indicated that no exception should be returned to the client.
10 | ///
11 | OK = 0x00,
12 |
13 | ///
14 | /// The function code received in the query is not an allowable action for the server.
15 | ///
16 | IllegalFunction = 0x01,
17 |
18 | ///
19 | /// The data address received in the query is not an allowable address for the server.
20 | ///
21 | IllegalDataAddress = 0x02,
22 |
23 | ///
24 | /// A value contained in the query data field is not an allowable value for server.
25 | ///
26 | IllegalDataValue = 0x03,
27 |
28 | ///
29 | /// An unrecoverable error occurred while the server was attempting to perform the requested action.
30 | ///
31 | ServerDeviceFailure = 0x04,
32 |
33 | ///
34 | /// Specialized use in conjunction with programming commands. The server has accepted the request and is processing it, but a long duration of time will be required to do so.
35 | ///
36 | Acknowledge = 0x05,
37 |
38 | ///
39 | /// Specialized use in conjunction with programming commands. The engaged in processing a long–duration program command.
40 | ///
41 | ServerDeviceBusy = 0x06,
42 |
43 | ///
44 | /// Specialized use in conjunction with function codes 20 and 21 and reference type 6, to indicate that the extended file area failed to pass a consistency check.
45 | ///
46 | MemoryParityError = 0x8,
47 |
48 | ///
49 | /// Specialized use in conjunction with gateways, indicates that the gateway was unable to allocate an internal communication path from the input port to the output port for processing the request.
50 | ///
51 | GatewayPathUnavailable = 0x0A,
52 |
53 | ///
54 | /// Specialized use in conjunction with gateways, indicates that no response was obtained from the target device.
55 | ///
56 | GatewayTargetDeviceFailedToRespond = 0x0B,
57 | }
58 |
--------------------------------------------------------------------------------
/doc/samples/modbus_validator.md:
--------------------------------------------------------------------------------
1 | # Modbus Validator
2 |
3 | The following code is an extension to the [Modbus TCP sample](modbus_tcp.md) but is just as valid for the RTU server. The `RequestValidator` property accepts a method which performs the validation each time a client sends a request to the server. This function will not replace the default checks but complement it. This means for example that the server will still check if the absolute registers limits are exceeded or an unknown function code is provided.
4 |
5 | ```cs
6 | var server = new ModbusTcpServer()
7 | {
8 | RequestValidator = this.ModbusValidator;
9 | };
10 |
11 | private ModbusExceptionCode ModbusValidator(RequestValidatorArgs args)
12 | {
13 | // check if address is within valid holding register limits
14 | var holdingLimits = args.Address >= 50 && args.Address < 90 ||
15 | args.Address >= 2000 && args.Address < 2100;
16 |
17 | // check if address is within valid input register limits
18 | var inputLimits = args.Address >= 1000 && args.Address < 2000;
19 |
20 | // go through all cases and return proper response
21 | return (args.FunctionCode, holdingLimits, inputLimits) switch
22 | {
23 | // holding registers
24 | (ModbusFunctionCode.ReadHoldingRegisters, true, _) => ModbusExceptionCode.OK,
25 | (ModbusFunctionCode.ReadWriteMultipleRegisters, true, _) => ModbusExceptionCode.OK,
26 | (ModbusFunctionCode.WriteMultipleRegisters, true, _) => ModbusExceptionCode.OK,
27 | (ModbusFunctionCode.WriteSingleRegister, true, _) => ModbusExceptionCode.OK,
28 |
29 | (ModbusFunctionCode.ReadHoldingRegisters, false, _) => ModbusExceptionCode.IllegalDataAddress,
30 | (ModbusFunctionCode.ReadWriteMultipleRegisters, false, _) => ModbusExceptionCode.IllegalDataAddress,
31 | (ModbusFunctionCode.WriteMultipleRegisters, false, _) => ModbusExceptionCode.IllegalDataAddress,
32 | (ModbusFunctionCode.WriteSingleRegister, false, _) => ModbusExceptionCode.IllegalDataAddress,
33 |
34 | // input registers
35 | (ModbusFunctionCode.ReadInputRegisters, _, true) => ModbusExceptionCode.OK,
36 | (ModbusFunctionCode.ReadInputRegisters, _, false) => ModbusExceptionCode.IllegalDataAddress,
37 |
38 | // deny other function codes
39 | _ => ModbusExceptionCode.IllegalFunction
40 | };
41 | }
42 | ```
--------------------------------------------------------------------------------
/src/FluentModbus/ExtendedBinaryReader.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | namespace FluentModbus;
4 |
5 | ///
6 | /// A binary reader with extended capability to handle big-endian data.
7 | ///
8 | public class ExtendedBinaryReader : BinaryReader
9 | {
10 | ///
11 | /// Initializes a new instance of the instance.
12 | ///
13 | /// The underlying data stream.
14 | public ExtendedBinaryReader(Stream stream) : base(stream)
15 | {
16 | //
17 | }
18 |
19 | ///
20 | /// Reads a signed short value from the stream.
21 | ///
22 | public short ReadInt16Reverse()
23 | {
24 | return ReadReverse(BitConverter.GetBytes(ReadInt16()));
25 | }
26 |
27 | ///
28 | /// Reads an unsigned short value from the stream.
29 | ///
30 | public ushort ReadUInt16Reverse()
31 | {
32 | return ReadReverse(BitConverter.GetBytes(ReadUInt16()));
33 | }
34 |
35 | ///
36 | /// Reads a signed integer value from the stream.
37 | ///
38 | public int ReadInt32Reverse()
39 | {
40 | return ReadReverse(BitConverter.GetBytes(ReadInt32()));
41 | }
42 |
43 | ///
44 | /// Reads an unsigned integer value from the stream.
45 | ///
46 | public uint ReadUInt32Reverse()
47 | {
48 | return ReadReverse(BitConverter.GetBytes(ReadUInt32()));
49 | }
50 |
51 | ///
52 | /// Reads a signed long value from the stream.
53 | ///
54 | public long ReadInt64Reverse()
55 | {
56 | return ReadReverse(BitConverter.GetBytes(ReadInt64()));
57 | }
58 |
59 | ///
60 | /// Reads an unsigned long value from the stream.
61 | ///
62 | public ulong ReadUInt64Reverse()
63 | {
64 | return ReadReverse(BitConverter.GetBytes(ReadUInt64()));
65 | }
66 |
67 | ///
68 | /// Reads a single value value from the stream.
69 | ///
70 | public float ReadFloat32Reverse()
71 | {
72 | return ReadReverse(BitConverter.GetBytes(ReadSingle()));
73 | }
74 |
75 | ///
76 | /// Reads a double value value from the stream.
77 | ///
78 | public double ReadFloat64Reverse()
79 | {
80 | return ReadReverse(BitConverter.GetBytes(ReadDouble()));
81 | }
82 |
83 | private T ReadReverse(byte[] data) where T : struct
84 | {
85 | data.AsSpan().Reverse();
86 |
87 | return MemoryMarshal.Cast(data)[0];
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## vNext - xxxx-xx-xx
2 |
3 | ### Breaking changes
4 | - The constructors of `ModbusTcpClient` and `ModbusRtuClient` have been unified so that both accept an `ILogger`.
5 |
6 | ## v5.2.0 - 2024-04-23
7 |
8 | ### Features
9 | - Make IsConnected an abstract member of ModbusClient (#115)
10 |
11 | ## v5.1.0 - 2024-02-21
12 |
13 | ### Features
14 | - Add option for raising event even if values of buffer have not changed (#96)
15 | - support for WriteMultipleCoils (#111)
16 |
17 | ### Bugs Fixed
18 | - Fixed propagation of cancellationToken (#100)
19 | - Fixed exception for malformed messages (#101)
20 | - typo in ModbusClient docstring (#95)
21 | - SampleServerClientTCP broken? (#102)
22 |
23 | ## v5.0.3 - 2023-08-03
24 |
25 | - The Modbus TCP server now returns the received unit identifier even when its own unit identifier is set to zero (the default) (solves #93).
26 | - The protected methods `AddUnit()` and `RemoveUnit()` have been made public.
27 |
28 | ## v5.0.2 - 2022-11-14
29 |
30 | ### Bugs Fixed
31 |
32 | - The Modbus RTU client did not correctly detect response frames (thanks @zhangchaoza, fixes https://github.com/Apollo3zehn/FluentModbus/issues/83)
33 |
34 | ## v5.0.1 - 2022-11-14
35 |
36 | ### Bugs Fixed
37 |
38 | - The Modbus RTU server did not correctly detect request frames (thanks @jmsqlr, https://github.com/Apollo3zehn/FluentModbus/pull/75#issuecomment-1304653670)
39 |
40 | ## v5.0.0 - 2022-09-08
41 |
42 | ### Breaking Changes
43 | - 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).
44 |
45 | ### Features
46 | - Modbus TCP and RTU clients implement `IDisposable` so you can do the following now: `using var client = new ModbusTcpClient(...)` (#67)
47 | - Modbus server base class has now a virtual `Stop` method so the actual server can be stopped using a base class reference (#79).
48 |
49 | ### Bugs Fixed
50 | - The Modbus server ignored the unit identifier and responded to all requests (#79).
51 | - Modbus server side read timeout exception handling is more defined now:
52 | - The TCP server closes the connection.
53 | - 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.
54 | - Modbus server did not properly handle asynchronous cancellation (#79).
55 |
56 | > [See API changes on Fuget.org](https://www.fuget.org/packages/FluentModbus/5.0.0/lib/netstandard2.1/diff/4.1.0/)
57 |
58 | Thanks @schotime and @LukasKarel for your PRs!
--------------------------------------------------------------------------------
/src/FluentModbus/Client/ModbusClientAsync.tt:
--------------------------------------------------------------------------------
1 | <#@ template language="C#" #>
2 | <#@ output extension=".cs" #>
3 | <#@ import namespace="System.IO" #>
4 | <#@ import namespace="System.Text.RegularExpressions" #>
5 |
6 | <#
7 | var csstring = File
8 | .ReadAllText("src/FluentModbus/Client/ModbusClient.cs")
9 | .Replace("\r\n", "\n");
10 |
11 | var methodsToConvert = Regex.Matches(csstring, @" \/{3}(?:.(?!\n\n))*?(?:extendFrame\);|\).*?\n })", RegexOptions.Singleline);
12 | #>
13 | /* This is automatically translated code. */
14 |
15 | #pragma warning disable CS1998
16 |
17 | using System.Collections;
18 | using System.Runtime.InteropServices;
19 |
20 | namespace FluentModbus;
21 |
22 | public abstract partial class ModbusClient
23 | {
24 | <#
25 | foreach (Match method in methodsToConvert)
26 | {
27 | var methodString = method.Value;
28 |
29 | // add cancellation token XML comment
30 | methodString = Regex.Replace(methodString, @"(>\r?\n) (public|protected)", m => $"{m.Groups[1]} /// The token to monitor for cancellation requests. The default value is .\n {m.Groups[2]}", RegexOptions.Singleline);
31 |
32 | // add cancellation token
33 | methodString = Regex.Replace(methodString, @"(>\r?\n (?:public|protected).*?)\((.*?)\)", m => $"{m.Groups[1]}({m.Groups[2]}, CancellationToken cancellationToken = default)");
34 |
35 | // replace return values
36 | methodString = Regex.Replace(methodString, " void", $" {(methodString.Contains("abstract") ? "" : "async")} Task");
37 | methodString = Regex.Replace(methodString, " Span<(.*?)>", m => $"{(methodString.Contains("abstract") ? "" : " async")} Task>");
38 |
39 | // replace method name
40 | methodString = Regex.Replace(methodString, @"(.* Task[^ ]*) (.*?)([<|\(])", m => $"{m.Groups[1]} {m.Groups[2]}Async{m.Groups[3]}");
41 |
42 | // replace TransceiveFrame
43 | methodString = Regex.Replace(methodString, @"(TransceiveFrame)(.*?\n })\);", m => $"await {m.Groups[1]}Async{m.Groups[2]}, cancellationToken).ConfigureAwait(false);", RegexOptions.Singleline);
44 |
45 | methodString = Regex.Replace(methodString, @"(TransceiveFrame)(.*?\n })\)\.Slice", m => $"(await {m.Groups[1]}Async{m.Groups[2]}, cancellationToken).ConfigureAwait(false)).Slice", RegexOptions.Singleline);
46 |
47 | // replace MemoryMarshal
48 | methodString = Regex.Replace(methodString, @"MemoryMarshal(.+?)\(((?:\r?\n)?.+?)\((.+?)\)\);", m => $"SpanExtensions{m.Groups[1]}(await {m.Groups[2]}Async({m.Groups[3]}, cancellationToken).ConfigureAwait(false));");
49 |
50 | // replace remaining (WriteXXXRegister(s))
51 | methodString = Regex.Replace(methodString, @"(\n )(Write.*?Registers?)\((.*?)\);", m => $"{m.Groups[1]}await {m.Groups[2]}Async({m.Groups[3]}, cancellationToken).ConfigureAwait(false);", RegexOptions.Singleline);
52 |
53 | methodString += "\n\n";
54 | Write(methodString);
55 | }
56 | #>
57 | }
58 |
59 | #pragma warning restore CS1998
--------------------------------------------------------------------------------
/.github/workflows/build-and-publish.yml:
--------------------------------------------------------------------------------
1 | name: Build and Publish
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - dev
8 |
9 | tags:
10 | - '*'
11 |
12 | jobs:
13 |
14 | build:
15 |
16 | name: Build
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v3
22 |
23 | - name: Fetch tags
24 | run: git fetch --tags --force
25 |
26 | - name: Metadata
27 | run: echo "IS_RELEASE=${{ startsWith(github.ref, 'refs/tags/') }}" >> $GITHUB_ENV
28 |
29 | - name: Environment
30 | run: |
31 | echo "VERSION=$(python build/print_version.py ${{ github.run_number }} ${{ env.IS_RELEASE }} false)" >> $GITHUB_ENV
32 | echo "$(python build/print_solution.py)" >> $GITHUB_ENV
33 |
34 | - name: Extract annotation tag
35 | if: ${{ env.IS_RELEASE == 'true' }}
36 | run: python build/create_tag_body.py
37 |
38 | - name: Build
39 | run: dotnet build -c Release src/FluentModbus/FluentModbus.csproj
40 |
41 | - name: Test
42 | run: dotnet test -c Release /p:BuildProjectReferences=false
43 |
44 | - name: Upload Artifacts
45 | uses: actions/upload-artifact@v4.4.0
46 | with:
47 | name: artifacts
48 | path: |
49 | artifacts/package/release/
50 | artifacts/tag_body.txt
51 |
52 | outputs:
53 | is_release: ${{ env.IS_RELEASE }}
54 | version: ${{ env.VERSION }}
55 |
56 | publish_dev:
57 |
58 | needs: build
59 | name: Publish (dev)
60 | runs-on: ubuntu-latest
61 |
62 | if: ${{ needs.build.outputs.is_release != 'true' }}
63 |
64 | steps:
65 |
66 | - name: Download Artifacts
67 | uses: actions/download-artifact@v4.1.7
68 | with:
69 | name: artifacts
70 | path: artifacts
71 |
72 | - name: Nuget package (MyGet)
73 | run: dotnet nuget push 'artifacts/package/release/*.nupkg' --api-key ${MYGET_API_KEY} --source https://www.myget.org/F/apollo3zehn-dev/api/v3/index.json
74 | env:
75 | MYGET_API_KEY: ${{ secrets.MYGET_API_KEY }}
76 |
77 | publish_release:
78 |
79 | needs: build
80 | name: Publish (release)
81 | runs-on: ubuntu-latest
82 |
83 | if: ${{ needs.build.outputs.is_release == 'true' }}
84 |
85 | steps:
86 |
87 | - name: Download Artifacts
88 | uses: actions/download-artifact@v4.1.7
89 | with:
90 | name: artifacts
91 | path: artifacts
92 |
93 | - name: GitHub Release Artifacts
94 | uses: softprops/action-gh-release@v1
95 | with:
96 | body_path: artifacts/tag_body.txt
97 |
98 | - name: Nuget package (Nuget)
99 | run: dotnet nuget push 'artifacts/package/release/*.nupkg' --api-key ${NUGET_API_KEY} --source https://api.nuget.org/v3/index.json
100 | env:
101 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
102 |
--------------------------------------------------------------------------------
/src/FluentModbus/ExtendedBinaryWriter.cs:
--------------------------------------------------------------------------------
1 | namespace FluentModbus;
2 |
3 | ///
4 | /// A binary writer with extended capability to handle big-endian data.
5 | ///
6 | public class ExtendedBinaryWriter : BinaryWriter
7 | {
8 | ///
9 | /// Initializes a new instance of the instance.
10 | ///
11 | /// The underlying data stream.
12 | public ExtendedBinaryWriter(Stream stream) : base(stream)
13 | {
14 | //
15 | }
16 |
17 | ///
18 | /// Writes the provided byte array to the stream.
19 | ///
20 | /// The data to be written.
21 | private void WriteReverse(byte[] data)
22 | {
23 | Array.Reverse(data);
24 | base.Write(data);
25 | }
26 |
27 | ///
28 | /// Writes the provided value to the stream.
29 | ///
30 | /// The value to be written.
31 | public void WriteReverse(short value)
32 | {
33 | WriteReverse(BitConverter.GetBytes(value));
34 | }
35 |
36 | ///
37 | /// Writes the provided value to the stream.
38 | ///
39 | /// The value to be written.
40 | public void WriteReverse(ushort value)
41 | {
42 | WriteReverse(BitConverter.GetBytes(value));
43 | }
44 |
45 | ///
46 | /// Writes the provided value to the stream.
47 | ///
48 | /// The value to be written.
49 | public void WriteReverse(int value)
50 | {
51 | WriteReverse(BitConverter.GetBytes(value));
52 | }
53 |
54 | ///
55 | /// Writes the provided value to the stream.
56 | ///
57 | /// The value to be written.
58 | public void WriteReverse(uint value)
59 | {
60 | WriteReverse(BitConverter.GetBytes(value));
61 | }
62 |
63 | ///
64 | /// Writes the provided value to the stream.
65 | ///
66 | /// The value to be written.
67 | public void WriteReverse(long value)
68 | {
69 | WriteReverse(BitConverter.GetBytes(value));
70 | }
71 |
72 | ///
73 | /// Writes the provided value to the stream.
74 | ///
75 | /// The value to be written.
76 | public void WriteReverse(ulong value)
77 | {
78 | WriteReverse(BitConverter.GetBytes(value));
79 | }
80 |
81 | ///
82 | /// Writes the provided value to the stream.
83 | ///
84 | /// The value to be written.
85 | public void WriteReverse(float value)
86 | {
87 | WriteReverse(BitConverter.GetBytes(value));
88 | }
89 |
90 | ///
91 | /// Writes the provided value to the stream.
92 | ///
93 | /// The value to be written.
94 | public void WriteReverse(double value)
95 | {
96 | WriteReverse(BitConverter.GetBytes(value));
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/FluentModbus/ModbusFunctionCode.cs:
--------------------------------------------------------------------------------
1 | namespace FluentModbus;
2 |
3 | ///
4 | /// Specifies the action the Modbus server is requested to do.
5 | ///
6 | public enum ModbusFunctionCode : byte
7 | {
8 | // class 0
9 |
10 | ///
11 | /// This function code is used to read the contents of a contiguous block of holding registers in a remote device.
12 | ///
13 | ReadHoldingRegisters = 0x03, // FC03
14 |
15 | ///
16 | /// This function code is used to write a block of contiguous registers (1 to 123 registers) in a remote device.
17 | ///
18 | WriteMultipleRegisters = 0x10, // FC16
19 |
20 | // class 1
21 | ///
22 | /// This function code is used to read from 1 to 2000 contiguous status of coils in a remote device.
23 | ///
24 | ReadCoils = 0x01, // FC01
25 |
26 | ///
27 | /// This function code is used to read from 1 to 2000 contiguous status of discrete inputs in a remote device.
28 | ///
29 | ReadDiscreteInputs = 0x02, // FC02
30 |
31 | ///
32 | /// This function code is used to read from 1 to 125 contiguous input registers in a remote device.
33 | ///
34 | ReadInputRegisters = 0x04, // FC04
35 |
36 | ///
37 | /// This function code is used to write a single output to either ON or OFF in a remote device.
38 | ///
39 | WriteSingleCoil = 0x05, // FC05
40 |
41 | ///
42 | /// This function code is used to write a single holding register in a remote device.
43 | ///
44 | WriteSingleRegister = 0x06, // FC06
45 |
46 | ///
47 | /// This function code is used to read the contents of eight Exception Status outputs in a remote device.
48 | ///
49 | ReadExceptionStatus = 0x07, // FC07 (Serial Line only)
50 |
51 | // class 2
52 |
53 | ///
54 | /// This function code is used to force each coil in a sequence of coils to either ON or OFF in a remote device.
55 | ///
56 | WriteMultipleCoils = 0x0F, // FC15
57 |
58 | ///
59 | /// This function code is used to perform a file record read.
60 | ///
61 | ReadFileRecord = 0x14, // FC20
62 |
63 | ///
64 | /// This function code is used to perform a file record write.
65 | ///
66 | WriteFileRecord = 0x15, // FC21
67 |
68 | ///
69 | /// This function code is used to modify the contents of a specified holding register using a combination of an AND mask, an OR mask, and the register's current contents.
70 | ///
71 | MaskWriteRegister = 0x16, // FC22
72 |
73 | ///
74 | /// This function code performs a combination of one read operation and one write operation in a single MODBUS transaction. The write operation is performed before the read.
75 | ///
76 | ReadWriteMultipleRegisters = 0x17, // FC23
77 |
78 | ///
79 | /// This function code allows to read the contents of a First-In-First-Out (FIFO) queue of register in a remote device.
80 | ///
81 | ReadFifoQueue = 0x18, // FC24
82 |
83 | //
84 | ///
85 | /// This function code is added to another function code to indicate that an error occured.
86 | ///
87 | Error = 0x80 // FC128
88 | }
89 |
--------------------------------------------------------------------------------
/tests/FluentModbus.Tests/ModbusUtilsTests.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using Xunit;
3 |
4 | namespace FluentModbus.Tests;
5 |
6 | public class ModbusUtilsTests : IClassFixture
7 | {
8 | [Theory]
9 | [InlineData("127.0.0.1:502", "127.0.0.1")]
10 | [InlineData("127.0.0.1:503", "127.0.0.1:503")]
11 | [InlineData("[::1]:502", "::1")]
12 | [InlineData("[::1]:503", "[::1]:503")]
13 | public void CanParseEndpoint(string expectedString, string endpoint)
14 | {
15 | // Arrange
16 | var expected = IPEndPoint.Parse(expectedString);
17 |
18 | // Act
19 | var success = ModbusUtils.TryParseEndpoint(endpoint, out var actual);
20 |
21 | // Assert
22 | Assert.True(success);
23 | Assert.Equal(expected, actual);
24 | }
25 |
26 | [Fact]
27 | public void CalculatesCrcCorrectly()
28 | {
29 | // Arrange
30 | var data = new byte[] { 0xA0, 0xB1, 0xC2 };
31 |
32 | // Act
33 | var expected = 0x2384;
34 | var actual = ModbusUtils.CalculateCRC(data);
35 |
36 | // Assert
37 | Assert.Equal(expected, actual);
38 | }
39 |
40 | [Fact]
41 | public void SwapsEndiannessShort()
42 | {
43 | // Arrange
44 | var data = (short)512;
45 |
46 | // Act
47 | var expected = (short)2;
48 | var actual = ModbusUtils.SwitchEndianness(data);
49 |
50 | // Assert
51 | Assert.Equal(expected, actual);
52 | }
53 |
54 | [Fact]
55 | public void SwapsEndiannessUShort()
56 | {
57 | // Arrange
58 | var data = (ushort)512;
59 |
60 | // Act
61 | var expected = (ushort)2;
62 | var actual = ModbusUtils.SwitchEndianness(data);
63 |
64 | // Assert
65 | Assert.Equal(expected, actual);
66 | }
67 |
68 | public static IList