├── .editorconfig ├── .github └── workflows │ └── build-and-publish.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── Directory.Build.props ├── FluentModbus.sln ├── LICENSE.md ├── README.md ├── build ├── create_tag_body.py ├── print_solution.py ├── print_version.py ├── release.py └── version.py ├── doc ├── dark-mode │ └── public │ │ └── main.js ├── docfx.json ├── filterConfig.yml ├── images │ ├── icon.ico │ ├── icon.png │ ├── logo.svg │ └── sample.png ├── index.md ├── samples │ ├── modbus_rtu.md │ ├── modbus_tcp.md │ ├── modbus_validator.md │ └── toc.yml └── toc.yml ├── sample ├── SampleServerClientRtu │ ├── Program.cs │ └── SampleServerClientRtu.csproj └── SampleServerClientTcp │ ├── Program.cs │ └── SampleServerClientTcp.csproj ├── solution.json ├── src ├── Directory.Build.props └── FluentModbus │ ├── CastMemoryManager.cs │ ├── Client │ ├── ModbusClient.cs │ ├── ModbusClientAsync.cs │ ├── ModbusClientAsync.tt │ ├── ModbusRtuClient.cs │ ├── ModbusRtuClientAsync.cs │ ├── ModbusRtuClientAsync.tt │ ├── ModbusRtuOverTcpClient.cs │ ├── ModbusRtuOverTcpClientAsync.cs │ ├── ModbusRtuOverTcpClientAsync.tt │ ├── ModbusTcpClient.cs │ ├── ModbusTcpClientAsync.cs │ └── ModbusTcpClientAsync.tt │ ├── ExtendedBinaryReader.cs │ ├── ExtendedBinaryWriter.cs │ ├── FluentModbus.csproj │ ├── IModbusRtuSerialPort.cs │ ├── IsExternalInit.cs │ ├── ModbusEndianness.cs │ ├── ModbusException.cs │ ├── ModbusExceptionCode.cs │ ├── ModbusFrameBuffer.cs │ ├── ModbusFunctionCode.cs │ ├── ModbusRtuSerialPort.cs │ ├── ModbusUtils.cs │ ├── Resources │ ├── ErrorMessage.Designer.cs │ └── ErrorMessage.resx │ ├── Server │ ├── DefaultTcpClientProvider.cs │ ├── ITcpClientProvider.cs │ ├── ModbusRequestHandler.cs │ ├── ModbusRtuRequestHandler.cs │ ├── ModbusRtuServer.cs │ ├── ModbusServer.cs │ ├── ModbusTcpRequestHandler.cs │ └── ModbusTcpServer.cs │ └── SpanExtensions.cs ├── tests ├── Directory.Build.props └── FluentModbus.Tests │ ├── FluentModbus.Tests.csproj │ ├── ModbusRtuServerTests.cs │ ├── ModbusServerTests.cs │ ├── ModbusTcpClientTests.cs │ ├── ModbusTcpServerTests.cs │ ├── ModbusUtilsTests.cs │ ├── ProtocolTests.cs │ ├── ProtocolTestsAsync.cs │ └── Support │ ├── EndpointSource.cs │ ├── FakeSerialPort.cs │ └── XUnitFixture.cs └── version.json /.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 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnet.defaultSolution": "FluentModbus.sln" 3 | } -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /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! -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /FluentModbus.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28803.202 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8449A872-2591-4C77-A810-974A20DD0227}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{2B4BA2BB-B561-4301-A1B9-9E59E58BE8EF}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{D6452DF6-D009-4A4D-A161-E25DE443C0D8}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleServerClientTcp", "sample\SampleServerClientTcp\SampleServerClientTcp.csproj", "{226ABEFD-EC87-42B1-9F7E-DDDA298C124D}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleServerClientRtu", "sample\SampleServerClientRtu\SampleServerClientRtu.csproj", "{ACFCBF6C-4363-47CB-B412-907464C7888F}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentModbus", "src\FluentModbus\FluentModbus.csproj", "{BE6DE3F6-ED5E-4603-AC45-8966CD4EB2D1}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentModbus.Tests", "tests\FluentModbus.Tests\FluentModbus.Tests.csproj", "{EDA7B16D-17C0-4E4F-8315-A639BE519B26}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Debug|x86 = Debug|x86 24 | Release|Any CPU = Release|Any CPU 25 | Release|x86 = Release|x86 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {226ABEFD-EC87-42B1-9F7E-DDDA298C124D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {226ABEFD-EC87-42B1-9F7E-DDDA298C124D}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {226ABEFD-EC87-42B1-9F7E-DDDA298C124D}.Debug|x86.ActiveCfg = Debug|Any CPU 31 | {226ABEFD-EC87-42B1-9F7E-DDDA298C124D}.Debug|x86.Build.0 = Debug|Any CPU 32 | {226ABEFD-EC87-42B1-9F7E-DDDA298C124D}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {226ABEFD-EC87-42B1-9F7E-DDDA298C124D}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {226ABEFD-EC87-42B1-9F7E-DDDA298C124D}.Release|x86.ActiveCfg = Release|Any CPU 35 | {226ABEFD-EC87-42B1-9F7E-DDDA298C124D}.Release|x86.Build.0 = Release|Any CPU 36 | {ACFCBF6C-4363-47CB-B412-907464C7888F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {ACFCBF6C-4363-47CB-B412-907464C7888F}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {ACFCBF6C-4363-47CB-B412-907464C7888F}.Debug|x86.ActiveCfg = Debug|Any CPU 39 | {ACFCBF6C-4363-47CB-B412-907464C7888F}.Debug|x86.Build.0 = Debug|Any CPU 40 | {ACFCBF6C-4363-47CB-B412-907464C7888F}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {ACFCBF6C-4363-47CB-B412-907464C7888F}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {ACFCBF6C-4363-47CB-B412-907464C7888F}.Release|x86.ActiveCfg = Release|Any CPU 43 | {ACFCBF6C-4363-47CB-B412-907464C7888F}.Release|x86.Build.0 = Release|Any CPU 44 | {BE6DE3F6-ED5E-4603-AC45-8966CD4EB2D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {BE6DE3F6-ED5E-4603-AC45-8966CD4EB2D1}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {BE6DE3F6-ED5E-4603-AC45-8966CD4EB2D1}.Debug|x86.ActiveCfg = Debug|Any CPU 47 | {BE6DE3F6-ED5E-4603-AC45-8966CD4EB2D1}.Debug|x86.Build.0 = Debug|Any CPU 48 | {BE6DE3F6-ED5E-4603-AC45-8966CD4EB2D1}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {BE6DE3F6-ED5E-4603-AC45-8966CD4EB2D1}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {BE6DE3F6-ED5E-4603-AC45-8966CD4EB2D1}.Release|x86.ActiveCfg = Release|Any CPU 51 | {BE6DE3F6-ED5E-4603-AC45-8966CD4EB2D1}.Release|x86.Build.0 = Release|Any CPU 52 | {EDA7B16D-17C0-4E4F-8315-A639BE519B26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {EDA7B16D-17C0-4E4F-8315-A639BE519B26}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {EDA7B16D-17C0-4E4F-8315-A639BE519B26}.Debug|x86.ActiveCfg = Debug|Any CPU 55 | {EDA7B16D-17C0-4E4F-8315-A639BE519B26}.Debug|x86.Build.0 = Debug|Any CPU 56 | {EDA7B16D-17C0-4E4F-8315-A639BE519B26}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {EDA7B16D-17C0-4E4F-8315-A639BE519B26}.Release|Any CPU.Build.0 = Release|Any CPU 58 | {EDA7B16D-17C0-4E4F-8315-A639BE519B26}.Release|x86.ActiveCfg = Release|Any CPU 59 | {EDA7B16D-17C0-4E4F-8315-A639BE519B26}.Release|x86.Build.0 = Release|Any CPU 60 | EndGlobalSection 61 | GlobalSection(SolutionProperties) = preSolution 62 | HideSolutionNode = FALSE 63 | EndGlobalSection 64 | GlobalSection(NestedProjects) = preSolution 65 | {226ABEFD-EC87-42B1-9F7E-DDDA298C124D} = {D6452DF6-D009-4A4D-A161-E25DE443C0D8} 66 | {ACFCBF6C-4363-47CB-B412-907464C7888F} = {D6452DF6-D009-4A4D-A161-E25DE443C0D8} 67 | {BE6DE3F6-ED5E-4603-AC45-8966CD4EB2D1} = {8449A872-2591-4C77-A810-974A20DD0227} 68 | {EDA7B16D-17C0-4E4F-8315-A639BE519B26} = {2B4BA2BB-B561-4301-A1B9-9E59E58BE8EF} 69 | EndGlobalSection 70 | GlobalSection(ExtensibilityGlobals) = postSolution 71 | SolutionGuid = {F998BD6D-3E2B-4AA0-A7A4-FD84100C45A4} 72 | EndGlobalSection 73 | EndGlobal 74 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FluentModbus 2 | 3 | [![GitHub Actions](https://github.com/Apollo3zehn/FluentModbus/actions/workflows/build-and-publish.yml/badge.svg)](https://github.com/Apollo3zehn/FluentModbus/actions) [![NuGet](https://img.shields.io/nuget/v/FluentModbus.svg?label=Nuget)](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 | ![Sample.](/doc/images/sample.png) -------------------------------------------------------------------------------- /build/create_tag_body.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import subprocess 4 | 5 | tag = os.getenv('GITHUB_REF_NAME') 6 | 7 | if tag is None: 8 | raise Exception("GITHUB_REF_NAME is not defined") 9 | 10 | os.mkdir("artifacts") 11 | 12 | with open("artifacts/tag_body.txt", "w") as file: 13 | 14 | output = subprocess.check_output(["git", "tag", "-l", "--format='%(contents)'", tag], stdin=None, stderr=None, shell=False) 15 | match = re.search("'(.*)'", output.decode("utf8"), re.DOTALL) 16 | 17 | if match is None: 18 | raise Exception("Unable to extract the tag body") 19 | 20 | tag_body = str(match.groups(1)[0]) 21 | file.write(tag_body) 22 | 23 | 24 | -------------------------------------------------------------------------------- /build/print_solution.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | with open("solution.json", "r") as fh: 4 | solution_data = json.load(fh) 5 | 6 | print( 7 | f""" 8 | AUTHORS={solution_data["authors"]} 9 | COMPANY={solution_data["company"]} 10 | COPYRIGHT={solution_data["copyright"]} 11 | PRODUCT={solution_data["product"]} 12 | PACKAGELICENSEEXPRESSION={solution_data["license"]} 13 | PACKAGEPROJECTURL={solution_data["project-url"]} 14 | REPOSITORYURL={solution_data["repository-url"]} 15 | """) -------------------------------------------------------------------------------- /build/print_version.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import version 4 | 5 | build = sys.argv[1] 6 | is_final_build = sys.argv[2] == "true" 7 | version_type = sys.argv[3] 8 | 9 | if is_final_build: 10 | build = None 11 | 12 | print(version.get_version(build, version_type)) 13 | -------------------------------------------------------------------------------- /build/release.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | import re 4 | import json 5 | 6 | import requests 7 | 8 | import version 9 | 10 | # get release version 11 | final_version = f"v{version.get_version(build=None)}" 12 | print(f"release version: {final_version}") 13 | 14 | # check if on master branch 15 | if not "* master" in subprocess.check_output(["git", "branch"], stdin=None, stderr=None, shell=False).decode("utf8"): 16 | raise Exception("Must be on master branch.") 17 | 18 | print(" master branch: OK") 19 | 20 | # check if release version already exist 21 | with open("solution.json", "r") as fh: 22 | solution_data = json.load(fh) 23 | 24 | pattern = r"https:\/\/github.com\/(.*)" 25 | request_url = re.sub(pattern, r"https://api.github.com/repos/\1/releases", solution_data["repository-url"]) 26 | 27 | headers = { 28 | "Authorization": f"token {sys.argv[1]}", 29 | "User-Agent": "Nexus", 30 | "Accept": "application/vnd.github.v3+json" 31 | } 32 | 33 | response = requests.get(request_url, headers=headers) 34 | response.raise_for_status() 35 | releases = response.json() 36 | 37 | if final_version in (release["name"] for release in releases): 38 | raise Exception(f"Release {final_version} already exists.") 39 | 40 | print(" unique release: OK") 41 | 42 | # get annotation 43 | with open("CHANGELOG.md") as file: 44 | changelog = file.read() 45 | 46 | matches = list(re.finditer(r"^##\s(.*?)\s-\s[0-9]{4}-[0-9]{2}-[0-9]{2}(.*?)(?=(?:\Z|^##\s))", changelog, re.MULTILINE | re.DOTALL)) 47 | 48 | if not matches: 49 | raise Exception(f"The file CHANGELOG.md is malformed.") 50 | 51 | match_for_version = next((match for match in matches if match[1] == final_version), None) 52 | 53 | if not match_for_version: 54 | raise Exception(f"No change log entry found for version {final_version}.") 55 | 56 | release_message = match_for_version[2].strip() 57 | 58 | print("extract annotation: OK") 59 | 60 | # create tag 61 | subprocess.check_output(["git", "tag", "-a", final_version, "-m", release_message, "--cleanup=whitespace"], stdin=None, stderr=None, shell=False) 62 | 63 | print(" create tag: OK") 64 | 65 | # push tag 66 | subprocess.check_output(["git", "push", "--quiet", "origin", final_version], stdin=None, stderr=None, shell=False) 67 | print(" push tag: OK") 68 | -------------------------------------------------------------------------------- /build/version.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Optional 3 | 4 | 5 | def get_version(build: Optional[str], version_type: str = "default") -> str: 6 | 7 | with open("version.json", "r") as fh: 8 | version_data = json.load(fh) 9 | 10 | # version 11 | version = version_data["version"] 12 | suffix = version_data["suffix"] 13 | 14 | if suffix: 15 | version = f"{version}-{suffix}" 16 | 17 | if build: 18 | 19 | # PEP440 does not support SemVer versioning (https://semver.org/#spec-item-9) 20 | if version_type == "pypi": 21 | version = f"{version}{int(build):03d}" 22 | 23 | else: 24 | version = f"{version}.{build}" 25 | 26 | return version 27 | -------------------------------------------------------------------------------- /doc/dark-mode/public/main.js: -------------------------------------------------------------------------------- 1 | export default { 2 | defaultTheme: 'dark' 3 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /doc/filterConfig.yml: -------------------------------------------------------------------------------- 1 | apiRules: 2 | 3 | - exclude: 4 | uidRegex: ^System\.Object 5 | type: Type -------------------------------------------------------------------------------- /doc/images/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apollo3zehn/FluentModbus/bbd7dcd1d628811a73328233700ca352b2d3d71c/doc/images/icon.ico -------------------------------------------------------------------------------- /doc/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apollo3zehn/FluentModbus/bbd7dcd1d628811a73328233700ca352b2d3d71c/doc/images/icon.png -------------------------------------------------------------------------------- /doc/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /doc/images/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apollo3zehn/FluentModbus/bbd7dcd1d628811a73328233700ca352b2d3d71c/doc/images/sample.png -------------------------------------------------------------------------------- /doc/samples/modbus_rtu.md: -------------------------------------------------------------------------------- 1 | # Modbus RTU 2 | 3 | The following sample show how a ModbusRTU [server](xref:FluentModbus.ModbusRtuServer) and [client](xref:FluentModbus.ModbusRtuClient) can interact with each other. The full sample is located [here](https://github.com/Apollo3zehn/FluentModbus/blob/master/sample/SampleServerClientRtu/Program.cs). 4 | 5 | ## Notes 6 | 7 | > [!NOTE] 8 | > In this sample, the server runs in ```asynchronous``` mode, i.e. the client request is answered immediately. Since this can potentially slow down the server when there are too many requests within short time, there is also the ```synchronous``` operating mode, which can be opted-in via ```new ModbusRtuServer(isAsynchronous: true)```. When you do so, the accumulated client requests are processed with a call to ```server.Update()```. See the [introduction](../index.md) for a short sample. 9 | 10 | ## Setup 11 | 12 | First, create the server and client instances and a logger to print the progress to the console: 13 | 14 | ```cs 15 | static async Task Main(string[] args) 16 | { 17 | /* Modbus RTU uses a COM port for communication. Therefore, to run 18 | * this sample, you need to make sure that there are real or virtual 19 | * COM ports available. The easiest way is to install one of the free 20 | * COM port bridges available in the internet. That way, the Modbus 21 | * server can connect to e.g. COM1 which is virtually linked to COM2, 22 | * where the client is connected to. 23 | * 24 | * When you only want to use the client and communicate to an external 25 | * Modbus server, simply remove all server related code parts in this 26 | * sample and connect to real COM port using only the client. 27 | */ 28 | 29 | /* define COM ports */ 30 | var serverPort = "COM1"; 31 | var clientPort = "COM2"; 32 | 33 | /* create logger */ 34 | var loggerFactory = LoggerFactory.Create(loggingBuilder => 35 | { 36 | loggingBuilder.SetMinimumLevel(LogLevel.Debug); 37 | loggingBuilder.AddConsole(); 38 | }); 39 | 40 | var serverLogger = loggerFactory.CreateLogger("Server"); 41 | var clientLogger = loggerFactory.CreateLogger("Client"); 42 | 43 | /* create Modbus RTU server */ 44 | using var server = new ModbusRtuServer(unitIdentifier: 1) 45 | { 46 | // see 'RegistersChanged' event below 47 | EnableRaisingEvents = true 48 | }; 49 | 50 | /* subscribe to the 'RegistersChanged' event (in case you need it) */ 51 | server.RegistersChanged += (sender, registerAddresses) => 52 | { 53 | // the variable 'registerAddresses' contains a list of modified register addresses 54 | }; 55 | 56 | /* create Modbus RTU client */ 57 | var client = new ModbusRtuClient(); 58 | ``` 59 | 60 | When everything is prepared, first start the server ... 61 | 62 | ```cs 63 | 64 | /* run Modbus RTU server */ 65 | var cts = new CancellationTokenSource(); 66 | 67 | var task_server = Task.Run(async () => 68 | { 69 | server.Start(serverPort); 70 | serverLogger.LogInformation("Server started."); 71 | 72 | while (!cts.IsCancellationRequested) 73 | { 74 | // lock is required to synchronize buffer access between this application and the Modbus client 75 | lock (server.Lock) 76 | { 77 | DoServerWork(server); 78 | } 79 | 80 | // update server buffer content once per second 81 | await Task.Delay(TimeSpan.FromSeconds(1)); 82 | } 83 | }, cts.Token); 84 | 85 | ``` 86 | 87 | ... and then the client: 88 | 89 | ```cs 90 | 91 | /* run Modbus RTU client */ 92 | var task_client = Task.Run(() => 93 | { 94 | client.Connect(clientPort); 95 | 96 | try 97 | { 98 | DoClientWork(client, clientLogger); 99 | } 100 | catch (Exception ex) 101 | { 102 | clientLogger.LogError(ex.Message); 103 | } 104 | 105 | client.Close(); 106 | 107 | Console.WriteLine("Tests finished. Press any key to continue."); 108 | Console.ReadKey(intercept: true); 109 | }); 110 | 111 | ``` 112 | 113 | Finally, wait for the the client to finish its work and then stop the server using the cancellation token source. 114 | 115 | ```cs 116 | // wait for client task to finish 117 | await task_client; 118 | 119 | // stop server 120 | cts.Cancel(); 121 | await task_server; 122 | 123 | server.Stop(); 124 | serverLogger.LogInformation("Server stopped."); 125 | } 126 | 127 | ``` 128 | 129 | ## Server operation 130 | 131 | While the server runs, the method below is executed periodically once per second. This method shows how the server can update it's registers to provide new data to the clients. 132 | 133 | ```cs 134 | static void DoServerWork(ModbusRtuServer server) 135 | { 136 | var random = new Random(); 137 | 138 | // Option A: normal performance version, more flexibility 139 | 140 | /* get buffer in standard form (Span) */ 141 | var registers = server.GetHoldingRegisters(); 142 | registers.SetLittleEndian(startingAddress: 5, random.Next()); 143 | 144 | // Option B: high performance version, less flexibility 145 | 146 | /* interpret buffer as array of bytes (8 bit) */ 147 | var byte_buffer = server.GetHoldingRegisterBuffer(); 148 | byte_buffer[20] = (byte)(random.Next() >> 24); 149 | 150 | /* interpret buffer as array of shorts (16 bit) */ 151 | var short_buffer = server.GetHoldingRegisterBuffer(); 152 | short_buffer[30] = (short)(random.Next(0, 100) >> 16); 153 | 154 | /* interpret buffer as array of ints (32 bit) */ 155 | var int_buffer = server.GetHoldingRegisterBuffer(); 156 | int_buffer[40] = random.Next(0, 100); 157 | } 158 | 159 | ``` 160 | 161 | # Client operation 162 | 163 | When the client is started, the method below is executed only once. After that, the client stops and the user is prompted to enter any key to finish the program. 164 | 165 | > [!NOTE] 166 | > All data is returned as ```Span```, which has great advantages but also some limitations as described in the repository's README. If you run into these limitations, simply call ```ToArray()``` to copy the data into a standard .NET array (e.g. ```var newData = data.ToArray();```) 167 | 168 | > [!NOTE] 169 | > Remember to always use the correct unit identifier (here: ```0x01```), which was assigned to the server when it was instantiated. 170 | 171 | ```cs 172 | static void DoClientWork(ModbusRtuClient client, ILogger logger) 173 | { 174 | Span data; 175 | 176 | var sleepTime = TimeSpan.FromMilliseconds(100); 177 | var unitIdentifier = 0x01; 178 | var startingAddress = 0; 179 | var registerAddress = 0; 180 | 181 | // ReadHoldingRegisters = 0x03, // FC03 182 | data = client.ReadHoldingRegisters(unitIdentifier, startingAddress, 10); 183 | logger.LogInformation("FC03 - ReadHoldingRegisters: Done"); 184 | Thread.Sleep(sleepTime); 185 | 186 | // WriteMultipleRegisters = 0x10, // FC16 187 | client.WriteMultipleRegisters(unitIdentifier, startingAddress, new byte[] { 10, 00, 20, 00, 30, 00, 255, 00, 255, 01 }); 188 | logger.LogInformation("FC16 - WriteMultipleRegisters: Done"); 189 | Thread.Sleep(sleepTime); 190 | 191 | // ReadCoils = 0x01, // FC01 192 | data = client.ReadCoils(unitIdentifier, startingAddress, 10); 193 | logger.LogInformation("FC01 - ReadCoils: Done"); 194 | Thread.Sleep(sleepTime); 195 | 196 | // ReadDiscreteInputs = 0x02, // FC02 197 | data = client.ReadDiscreteInputs(unitIdentifier, startingAddress, 10); 198 | logger.LogInformation("FC02 - ReadDiscreteInputs: Done"); 199 | Thread.Sleep(sleepTime); 200 | 201 | // ReadInputRegisters = 0x04, // FC04 202 | data = client.ReadInputRegisters(unitIdentifier, startingAddress, 10); 203 | logger.LogInformation("FC04 - ReadInputRegisters: Done"); 204 | Thread.Sleep(sleepTime); 205 | 206 | // WriteSingleCoil = 0x05, // FC05 207 | client.WriteSingleCoil(unitIdentifier, registerAddress, true); 208 | logger.LogInformation("FC05 - WriteSingleCoil: Done"); 209 | Thread.Sleep(sleepTime); 210 | 211 | // WriteSingleRegister = 0x06, // FC06 212 | client.WriteSingleRegister(unitIdentifier, registerAddress, 127); 213 | logger.LogInformation("FC06 - WriteSingleRegister: Done"); 214 | } 215 | ``` -------------------------------------------------------------------------------- /doc/samples/modbus_tcp.md: -------------------------------------------------------------------------------- 1 | # Modbus TCP 2 | 3 | The following sample show how a ModbusTCP [server](xref:FluentModbus.ModbusTcpServer) and [client](xref:FluentModbus.ModbusTcpClient) can interact with each other. The full sample is located [here](https://github.com/Apollo3zehn/FluentModbus/blob/master/sample/SampleServerClientTcp/Program.cs). 4 | 5 | ## Notes 6 | 7 | > [!NOTE] 8 | > In this sample, the server runs in ```asynchronous``` mode, i.e. all client requests are answered immediately. Since this can potentially slow down the server when there are too many requests within short time, there is also the ```synchronous``` operating mode, which can be opted-in via ```new ModbusTcpServer(isAsynchronous: true)```. When you do so, the accumulated client requests are processed with a call to ```server.Update()```. See the [introduction](../index.md) for a short sample. 9 | 10 | ## Setup 11 | 12 | First, create the server and client instances and a logger to print the progress to the console: 13 | 14 | ```cs 15 | static async Task Main(string[] args) 16 | { 17 | /* create logger */ 18 | var loggerFactory = LoggerFactory.Create(loggingBuilder => 19 | { 20 | loggingBuilder.SetMinimumLevel(LogLevel.Debug); 21 | loggingBuilder.AddConsole(); 22 | }); 23 | 24 | var serverLogger = loggerFactory.CreateLogger("Server"); 25 | var clientLogger = loggerFactory.CreateLogger("Client"); 26 | 27 | /* create Modbus TCP server */ 28 | using var server = new ModbusTcpServer(serverLogger) 29 | { 30 | // see 'RegistersChanged' event below 31 | EnableRaisingEvents = true 32 | }; 33 | 34 | /* subscribe to the 'RegistersChanged' event (in case you need it) */ 35 | server.RegistersChanged += (sender, registerAddresses) => 36 | { 37 | // the variable 'registerAddresses' contains a list of modified register addresses 38 | }; 39 | 40 | /* create Modbus TCP client */ 41 | var client = new ModbusTcpClient(); 42 | 43 | ``` 44 | 45 | When everything is prepared, first start the server ... 46 | 47 | ```cs 48 | 49 | /* run Modbus TCP server */ 50 | var cts = new CancellationTokenSource(); 51 | 52 | var task_server = Task.Run(async () => 53 | { 54 | server.Start(); 55 | serverLogger.LogInformation("Server started."); 56 | 57 | while (!cts.IsCancellationRequested) 58 | { 59 | // lock is required to synchronize buffer access between this application and one or more Modbus clients 60 | lock (server.Lock) 61 | { 62 | DoServerWork(server); 63 | } 64 | 65 | // update server buffer content once per second 66 | await Task.Delay(TimeSpan.FromSeconds(1)); 67 | } 68 | }, cts.Token); 69 | 70 | ``` 71 | 72 | ... and then the client: 73 | 74 | ```cs 75 | 76 | /* run Modbus TCP client */ 77 | var task_client = Task.Run(() => 78 | { 79 | client.Connect(); 80 | 81 | try 82 | { 83 | DoClientWork(client, clientLogger); 84 | } 85 | catch (Exception ex) 86 | { 87 | clientLogger.LogError(ex.Message); 88 | } 89 | 90 | client.Disconnect(); 91 | 92 | Console.WriteLine("Tests finished. Press any key to continue."); 93 | Console.ReadKey(intercept: true); 94 | }); 95 | 96 | ``` 97 | 98 | Finally, wait for the the client to finish its work and then stop the server using the cancellation token source. 99 | 100 | ```cs 101 | // wait for client task to finish 102 | await task_client; 103 | 104 | // stop server 105 | cts.Cancel(); 106 | await task_server; 107 | 108 | server.Stop(); 109 | serverLogger.LogInformation("Server stopped."); 110 | } 111 | 112 | ``` 113 | 114 | ## Server operation 115 | 116 | While the server runs, the method below is executed periodically once per second. This method shows how the server can update it's registers to provide new data to the clients. 117 | 118 | ```cs 119 | static void DoServerWork(ModbusTcpServer server) 120 | { 121 | var random = new Random(); 122 | 123 | // Option A: normal performance version, more flexibility 124 | 125 | /* get buffer in standard form (Span) */ 126 | var registers = server.GetHoldingRegisters(); 127 | registers.SetLittleEndian(startingAddress: 5, random.Next()); 128 | 129 | // Option B: high performance version, less flexibility 130 | 131 | /* interpret buffer as array of bytes (8 bit) */ 132 | var byte_buffer = server.GetHoldingRegisterBuffer(); 133 | byte_buffer[20] = (byte)(random.Next() >> 24); 134 | 135 | /* interpret buffer as array of shorts (16 bit) */ 136 | var short_buffer = server.GetHoldingRegisterBuffer(); 137 | short_buffer[30] = (short)(random.Next(0, 100) >> 16); 138 | 139 | /* interpret buffer as array of ints (32 bit) */ 140 | var int_buffer = server.GetHoldingRegisterBuffer(); 141 | int_buffer[40] = random.Next(0, 100); 142 | } 143 | 144 | ``` 145 | 146 | # Client operation 147 | 148 | When the client is started, the method below is executed only once. After that, the client stops and the user is prompted to enter any key to finish the program. 149 | 150 | > [!NOTE] 151 | > All data is returned as ```Span```, which has great advantages but also some limitations as described in the repository's README. If you run into these limitations, simply call ```ToArray()``` to copy the data into a standard .NET array (e.g. ```var newData = data.ToArray();```) 152 | 153 | ```cs 154 | static void DoClientWork(ModbusTcpClient client, ILogger logger) 155 | { 156 | Span data; 157 | 158 | var sleepTime = TimeSpan.FromMilliseconds(100); 159 | var unitIdentifier = 0x00; 160 | var startingAddress = 0; 161 | var registerAddress = 0; 162 | 163 | // ReadHoldingRegisters = 0x03, // FC03 164 | data = client.ReadHoldingRegisters(unitIdentifier, startingAddress, 10); 165 | logger.LogInformation("FC03 - ReadHoldingRegisters: Done"); 166 | Thread.Sleep(sleepTime); 167 | 168 | // WriteMultipleRegisters = 0x10, // FC16 169 | client.WriteMultipleRegisters(unitIdentifier, startingAddress, new byte[] { 10, 00, 20, 00, 30, 00, 255, 00, 255, 01 }); 170 | logger.LogInformation("FC16 - WriteMultipleRegisters: Done"); 171 | Thread.Sleep(sleepTime); 172 | 173 | // ReadCoils = 0x01, // FC01 174 | data = client.ReadCoils(unitIdentifier, startingAddress, 10); 175 | logger.LogInformation("FC01 - ReadCoils: Done"); 176 | Thread.Sleep(sleepTime); 177 | 178 | // ReadDiscreteInputs = 0x02, // FC02 179 | data = client.ReadDiscreteInputs(unitIdentifier, startingAddress, 10); 180 | logger.LogInformation("FC02 - ReadDiscreteInputs: Done"); 181 | Thread.Sleep(sleepTime); 182 | 183 | // ReadInputRegisters = 0x04, // FC04 184 | data = client.ReadInputRegisters(unitIdentifier, startingAddress, 10); 185 | logger.LogInformation("FC04 - ReadInputRegisters: Done"); 186 | Thread.Sleep(sleepTime); 187 | 188 | // WriteSingleCoil = 0x05, // FC05 189 | client.WriteSingleCoil(unitIdentifier, registerAddress, true); 190 | logger.LogInformation("FC05 - WriteSingleCoil: Done"); 191 | Thread.Sleep(sleepTime); 192 | 193 | // WriteSingleRegister = 0x06, // FC06 194 | client.WriteSingleRegister(unitIdentifier, registerAddress, 127); 195 | logger.LogInformation("FC06 - WriteSingleRegister: Done"); 196 | } 197 | ``` -------------------------------------------------------------------------------- /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 | ``` -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /doc/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Samples 2 | href: samples/ 3 | 4 | - name: API 5 | href: api/ 6 | -------------------------------------------------------------------------------- /sample/SampleServerClientRtu/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace FluentModbus.SampleMaster; 4 | 5 | class Program 6 | { 7 | static async Task Main(string[] args) 8 | { 9 | /* Modbus RTU uses a COM port for communication. Therefore, to run 10 | * this sample, you need to make sure that there are real or virtual 11 | * COM ports available. The easiest way is to install one of the free 12 | * COM port bridges available in the internet. That way, the Modbus 13 | * server can connect to e.g. COM1 which is virtually linked to COM2, 14 | * where the client is connected to. 15 | * 16 | * When you only want to use the client and communicate to an external 17 | * Modbus server, simply remove all server related code parts in this 18 | * sample and connect to real COM port using only the client. 19 | */ 20 | 21 | /* define COM ports */ 22 | var serverPort = "COM1"; 23 | var clientPort = "COM2"; 24 | 25 | /* create logger */ 26 | var loggerFactory = LoggerFactory.Create(loggingBuilder => 27 | { 28 | loggingBuilder.SetMinimumLevel(LogLevel.Debug); 29 | loggingBuilder.AddConsole(); 30 | }); 31 | 32 | var serverLogger = loggerFactory.CreateLogger("Server"); 33 | var clientLogger = loggerFactory.CreateLogger("Client"); 34 | 35 | /* create Modbus RTU server */ 36 | using var server = new ModbusRtuServer(unitIdentifier: 1) 37 | { 38 | // see 'RegistersChanged' event below 39 | EnableRaisingEvents = true 40 | }; 41 | 42 | /* subscribe to the 'RegistersChanged' event (in case you need it) */ 43 | server.RegistersChanged += (sender, registerAddresses) => 44 | { 45 | // the variable 'registerAddresses' contains the unit ID and a list of modified register addresses 46 | }; 47 | 48 | /* create Modbus RTU client */ 49 | var client = new ModbusRtuClient(); 50 | 51 | /* run Modbus RTU server */ 52 | var cts = new CancellationTokenSource(); 53 | server.Start(serverPort); 54 | serverLogger.LogInformation("Server started."); 55 | 56 | var task_server = Task.Run(async () => 57 | { 58 | while (!cts.IsCancellationRequested) 59 | { 60 | // lock is required to synchronize buffer access between this application and the Modbus client 61 | lock (server.Lock) 62 | { 63 | DoServerWork(server); 64 | } 65 | 66 | // update server register content once per second 67 | await Task.Delay(TimeSpan.FromSeconds(1)); 68 | } 69 | }, cts.Token); 70 | 71 | /* run Modbus RTU client */ 72 | var task_client = Task.Run(() => 73 | { 74 | client.Connect(clientPort); 75 | 76 | try 77 | { 78 | DoClientWork(client, clientLogger); 79 | } 80 | catch (Exception ex) 81 | { 82 | clientLogger.LogError(ex.Message); 83 | } 84 | 85 | client.Close(); 86 | 87 | Console.WriteLine("Tests finished. Press any key to continue."); 88 | Console.ReadKey(intercept: true); 89 | }); 90 | 91 | // wait for client task to finish 92 | await task_client; 93 | 94 | // stop server 95 | cts.Cancel(); 96 | await task_server; 97 | 98 | server.Stop(); 99 | serverLogger.LogInformation("Server stopped."); 100 | } 101 | 102 | static void DoServerWork(ModbusRtuServer server) 103 | { 104 | var random = new Random(); 105 | 106 | // Option A: normal performance version, more flexibility 107 | 108 | /* get buffer in standard form (Span) */ 109 | var registers = server.GetHoldingRegisters(); 110 | registers.SetLittleEndian(address: 5, random.Next()); 111 | 112 | // Option B: high performance version, less flexibility 113 | 114 | /* interpret buffer as array of bytes (8 bit) */ 115 | var byte_buffer = server.GetHoldingRegisterBuffer(); 116 | byte_buffer[20] = (byte)(random.Next() >> 24); 117 | 118 | /* interpret buffer as array of shorts (16 bit) */ 119 | var short_buffer = server.GetHoldingRegisterBuffer(); 120 | short_buffer[30] = (short)(random.Next(0, 100) >> 16); 121 | 122 | /* interpret buffer as array of ints (32 bit) */ 123 | var int_buffer = server.GetHoldingRegisterBuffer(); 124 | int_buffer[40] = random.Next(0, 100); 125 | } 126 | 127 | static void DoClientWork(ModbusRtuClient client, ILogger logger) 128 | { 129 | Span data; 130 | 131 | var sleepTime = TimeSpan.FromMilliseconds(100); 132 | var unitIdentifier = 0x01; 133 | var startingAddress = 0; 134 | var registerAddress = 0; 135 | 136 | // ReadHoldingRegisters = 0x03, // FC03 137 | data = client.ReadHoldingRegisters(unitIdentifier, startingAddress, 10); 138 | logger.LogInformation("FC03 - ReadHoldingRegisters: Done"); 139 | Thread.Sleep(sleepTime); 140 | 141 | // WriteMultipleRegisters = 0x10, // FC16 142 | client.WriteMultipleRegisters(unitIdentifier, startingAddress, new byte[] { 10, 00, 20, 00, 30, 00, 255, 00, 255, 01 }); 143 | logger.LogInformation("FC16 - WriteMultipleRegisters: Done"); 144 | Thread.Sleep(sleepTime); 145 | 146 | // ReadCoils = 0x01, // FC01 147 | data = client.ReadCoils(unitIdentifier, startingAddress, 10); 148 | logger.LogInformation("FC01 - ReadCoils: Done"); 149 | Thread.Sleep(sleepTime); 150 | 151 | // ReadDiscreteInputs = 0x02, // FC02 152 | data = client.ReadDiscreteInputs(unitIdentifier, startingAddress, 10); 153 | logger.LogInformation("FC02 - ReadDiscreteInputs: Done"); 154 | Thread.Sleep(sleepTime); 155 | 156 | // ReadInputRegisters = 0x04, // FC04 157 | data = client.ReadInputRegisters(unitIdentifier, startingAddress, 10); 158 | logger.LogInformation("FC04 - ReadInputRegisters: Done"); 159 | Thread.Sleep(sleepTime); 160 | 161 | // WriteSingleCoil = 0x05, // FC05 162 | client.WriteSingleCoil(unitIdentifier, registerAddress, true); 163 | logger.LogInformation("FC05 - WriteSingleCoil: Done"); 164 | Thread.Sleep(sleepTime); 165 | 166 | // WriteSingleRegister = 0x06, // FC06 167 | client.WriteSingleRegister(unitIdentifier, registerAddress, 127); 168 | logger.LogInformation("FC06 - WriteSingleRegister: Done"); 169 | } 170 | } -------------------------------------------------------------------------------- /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/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace FluentModbus.SampleMaster; 4 | 5 | class Program 6 | { 7 | static async Task Main(string[] args) 8 | { 9 | /* create logger */ 10 | var loggerFactory = LoggerFactory.Create(loggingBuilder => 11 | { 12 | loggingBuilder.SetMinimumLevel(LogLevel.Debug); 13 | loggingBuilder.AddConsole(); 14 | }); 15 | 16 | var serverLogger = loggerFactory.CreateLogger("Server"); 17 | var clientLogger = loggerFactory.CreateLogger("Client"); 18 | 19 | /* create Modbus TCP server */ 20 | using var server = new ModbusTcpServer(serverLogger) 21 | { 22 | // see 'RegistersChanged' event below 23 | EnableRaisingEvents = true 24 | }; 25 | 26 | /* subscribe to the 'RegistersChanged' event (in case you need it) */ 27 | server.RegistersChanged += (sender, registerAddresses) => 28 | { 29 | // the variable 'registerAddresses' contains a list of modified register addresses 30 | }; 31 | 32 | /* create Modbus TCP client */ 33 | var client = new ModbusTcpClient(); 34 | 35 | /* run Modbus TCP server */ 36 | var cts = new CancellationTokenSource(); 37 | server.Start(); 38 | serverLogger.LogInformation("Server started."); 39 | 40 | var task_server = Task.Run(async () => 41 | { 42 | while (!cts.IsCancellationRequested) 43 | { 44 | // lock is required to synchronize buffer access between this application and one or more Modbus clients 45 | lock (server.Lock) 46 | { 47 | DoServerWork(server); 48 | } 49 | 50 | // update server register content once per second 51 | await Task.Delay(TimeSpan.FromSeconds(1)); 52 | } 53 | }, cts.Token); 54 | 55 | /* run Modbus TCP client */ 56 | var task_client = Task.Run(() => 57 | { 58 | client.Connect(); 59 | 60 | try 61 | { 62 | DoClientWork(client, clientLogger); 63 | } 64 | catch (Exception ex) 65 | { 66 | clientLogger.LogError(ex.Message); 67 | } 68 | 69 | client.Disconnect(); 70 | 71 | Console.WriteLine("Tests finished. Press any key to continue."); 72 | Console.ReadKey(intercept: true); 73 | }); 74 | 75 | // wait for client task to finish 76 | await task_client; 77 | 78 | // stop server 79 | cts.Cancel(); 80 | await task_server; 81 | 82 | server.Stop(); 83 | serverLogger.LogInformation("Server stopped."); 84 | } 85 | 86 | static void DoServerWork(ModbusTcpServer server) 87 | { 88 | var random = new Random(); 89 | 90 | // Option A: normal performance version, more flexibility 91 | 92 | /* get buffer in standard form (Span) */ 93 | var registers = server.GetHoldingRegisters(); 94 | registers.SetLittleEndian(address: 5, random.Next()); 95 | 96 | // Option B: high performance version, less flexibility 97 | 98 | /* interpret buffer as array of bytes (8 bit) */ 99 | var byte_buffer = server.GetHoldingRegisterBuffer(); 100 | byte_buffer[20] = (byte)(random.Next() >> 24); 101 | 102 | /* interpret buffer as array of shorts (16 bit) */ 103 | var short_buffer = server.GetHoldingRegisterBuffer(); 104 | short_buffer[30] = (short)(random.Next(0, 100) >> 16); 105 | 106 | /* interpret buffer as array of ints (32 bit) */ 107 | var int_buffer = server.GetHoldingRegisterBuffer(); 108 | int_buffer[40] = random.Next(0, 100); 109 | } 110 | 111 | static void DoClientWork(ModbusTcpClient client, ILogger logger) 112 | { 113 | Span data; 114 | 115 | var sleepTime = TimeSpan.FromMilliseconds(100); 116 | var unitIdentifier = 0x00; 117 | var startingAddress = 0; 118 | var registerAddress = 0; 119 | 120 | // ReadHoldingRegisters = 0x03, // FC03 121 | data = client.ReadHoldingRegisters(unitIdentifier, startingAddress, 10); 122 | logger.LogInformation("FC03 - ReadHoldingRegisters: Done"); 123 | Thread.Sleep(sleepTime); 124 | 125 | // WriteMultipleRegisters = 0x10, // FC16 126 | client.WriteMultipleRegisters(unitIdentifier, startingAddress, new byte[] { 10, 00, 20, 00, 30, 00, 255, 00, 255, 01 }); 127 | logger.LogInformation("FC16 - WriteMultipleRegisters: Done"); 128 | Thread.Sleep(sleepTime); 129 | 130 | // ReadCoils = 0x01, // FC01 131 | data = client.ReadCoils(unitIdentifier, startingAddress, 10); 132 | logger.LogInformation("FC01 - ReadCoils: Done"); 133 | Thread.Sleep(sleepTime); 134 | 135 | // ReadDiscreteInputs = 0x02, // FC02 136 | data = client.ReadDiscreteInputs(unitIdentifier, startingAddress, 10); 137 | logger.LogInformation("FC02 - ReadDiscreteInputs: Done"); 138 | Thread.Sleep(sleepTime); 139 | 140 | // ReadInputRegisters = 0x04, // FC04 141 | data = client.ReadInputRegisters(unitIdentifier, startingAddress, 10); 142 | logger.LogInformation("FC04 - ReadInputRegisters: Done"); 143 | Thread.Sleep(sleepTime); 144 | 145 | // WriteSingleCoil = 0x05, // FC05 146 | client.WriteSingleCoil(unitIdentifier, registerAddress, true); 147 | logger.LogInformation("FC05 - WriteSingleCoil: Done"); 148 | Thread.Sleep(sleepTime); 149 | 150 | // WriteSingleRegister = 0x06, // FC06 151 | client.WriteSingleRegister(unitIdentifier, registerAddress, 127); 152 | logger.LogInformation("FC06 - WriteSingleRegister: Done"); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /src/FluentModbus/Client/ModbusRtuClient.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Ports; 2 | 3 | namespace FluentModbus; 4 | 5 | /// 6 | /// A Modbus RTU client. 7 | /// 8 | public partial class ModbusRtuClient : ModbusClient, IDisposable 9 | { 10 | #region Field 11 | 12 | private (IModbusRtuSerialPort Value, bool IsInternal)? _serialPort; 13 | private ModbusFrameBuffer _frameBuffer = default!; 14 | 15 | #endregion 16 | 17 | #region Constructors 18 | 19 | /// 20 | /// Creates a new Modbus RTU client for communication with Modbus RTU servers or bridges, routers and gateways for communication with TCP end units. 21 | /// 22 | public ModbusRtuClient() 23 | { 24 | // 25 | } 26 | 27 | #endregion 28 | 29 | #region Properties 30 | 31 | /// 32 | /// Gets the connection status of the underlying serial port. 33 | /// 34 | public override bool IsConnected => _serialPort?.Value.IsOpen ?? false; 35 | 36 | /// 37 | /// Gets or sets the serial baud rate. Default is 9600. 38 | /// 39 | public int BaudRate { get; set; } = 9600; 40 | 41 | /// 42 | /// Gets or sets the handshaking protocol for serial port transmission of data. Default is Handshake.None. 43 | /// 44 | public Handshake Handshake { get; set; } = Handshake.None; 45 | 46 | /// 47 | /// Gets or sets the parity-checking protocol. Default is Parity.Even. 48 | /// 49 | // Must be even according to the spec (https://www.modbus.org/docs/Modbus_over_serial_line_V1_02.pdf, 50 | // section 2.5.1 RTU Transmission Mode): "The default parity mode must be even parity." 51 | public Parity Parity { get; set; } = Parity.Even; 52 | 53 | /// 54 | /// Gets or sets the standard number of stopbits per byte. Default is StopBits.One. 55 | /// 56 | public StopBits StopBits { get; set; } = StopBits.One; 57 | 58 | /// 59 | /// Gets or sets the read timeout in milliseconds. Default is 1000 ms. 60 | /// 61 | public int ReadTimeout { get; set; } = 1000; 62 | 63 | /// 64 | /// Gets or sets the write timeout in milliseconds. Default is 1000 ms. 65 | /// 66 | public int WriteTimeout { get; set; } = 1000; 67 | 68 | #endregion 69 | 70 | #region Methods 71 | 72 | /// 73 | /// Connect to the specified with as default byte layout. 74 | /// 75 | /// The COM port to be used, e.g. COM1. 76 | public void Connect(string port) 77 | { 78 | Connect(port, ModbusEndianness.LittleEndian); 79 | } 80 | 81 | /// 82 | /// Connect to the specified . 83 | /// 84 | /// The COM port to be used, e.g. COM1. 85 | /// Specifies the endianness of the data exchanged with the Modbus server. 86 | public void Connect(string port, ModbusEndianness endianness) 87 | { 88 | var serialPort = new ModbusRtuSerialPort(new SerialPort(port) 89 | { 90 | BaudRate = BaudRate, 91 | Handshake = Handshake, 92 | Parity = Parity, 93 | StopBits = StopBits, 94 | ReadTimeout = ReadTimeout, 95 | WriteTimeout = WriteTimeout 96 | }); 97 | 98 | Initialize(serialPort, isInternal: true, endianness); 99 | } 100 | 101 | /// 102 | /// Initialize the Modbus TCP client with an externally managed . 103 | /// 104 | /// The externally managed . 105 | /// Specifies the endianness of the data exchanged with the Modbus server. 106 | public void Initialize(IModbusRtuSerialPort serialPort, ModbusEndianness endianness) 107 | { 108 | Initialize(serialPort, isInternal: false, endianness); 109 | } 110 | 111 | private void Initialize(IModbusRtuSerialPort serialPort, bool isInternal, ModbusEndianness endianness) 112 | { 113 | /* According to the spec (https://www.modbus.org/docs/Modbus_over_serial_line_V1_02.pdf), 114 | * section 2.5.1 RTU Transmission Mode: "... the use of no parity requires 2 stop bits." 115 | * Remove this check to improve compatibility (#56). 116 | */ 117 | 118 | //if (Parity == Parity.None && StopBits != StopBits.Two) 119 | // throw new InvalidOperationException(ErrorMessage.Modbus_NoParityRequiresTwoStopBits); 120 | 121 | SwapBytes = 122 | BitConverter.IsLittleEndian && endianness == ModbusEndianness.BigEndian || 123 | !BitConverter.IsLittleEndian && endianness == ModbusEndianness.LittleEndian; 124 | 125 | _frameBuffer = new ModbusFrameBuffer(256); 126 | 127 | if (_serialPort.HasValue && _serialPort.Value.IsInternal) 128 | _serialPort.Value.Value.Close(); 129 | 130 | _serialPort = (serialPort, isInternal); 131 | 132 | if (!serialPort.IsOpen) 133 | serialPort.Open(); 134 | } 135 | 136 | /// 137 | /// Closes the opened COM port and frees all resources. 138 | /// 139 | public void Close() 140 | { 141 | if (_serialPort.HasValue && _serialPort.Value.IsInternal) 142 | _serialPort.Value.Value.Close(); 143 | 144 | _frameBuffer?.Dispose(); 145 | } 146 | 147 | /// 148 | protected override Span TransceiveFrame(byte unitIdentifier, ModbusFunctionCode functionCode, Action extendFrame) 149 | { 150 | // WARNING: IF YOU EDIT THIS METHOD, REFLECT ALL CHANGES ALSO IN TransceiveFrameAsync! 151 | 152 | // build request 153 | if (!(0 <= unitIdentifier && unitIdentifier <= 247)) 154 | throw new ModbusException(ErrorMessage.ModbusClient_InvalidUnitIdentifier); 155 | 156 | // special case: broadcast (only for write commands) 157 | if (unitIdentifier == 0) 158 | { 159 | switch (functionCode) 160 | { 161 | case ModbusFunctionCode.WriteMultipleRegisters: 162 | case ModbusFunctionCode.WriteSingleCoil: 163 | case ModbusFunctionCode.WriteSingleRegister: 164 | case ModbusFunctionCode.WriteMultipleCoils: 165 | case ModbusFunctionCode.WriteFileRecord: 166 | case ModbusFunctionCode.MaskWriteRegister: 167 | break; 168 | default: 169 | throw new ModbusException(ErrorMessage.Modbus_InvalidUseOfBroadcast); 170 | } 171 | } 172 | 173 | _frameBuffer.Writer.Seek(0, SeekOrigin.Begin); 174 | _frameBuffer.Writer.Write(unitIdentifier); // 00 Unit Identifier 175 | extendFrame(_frameBuffer.Writer); 176 | 177 | var frameLength = (int)_frameBuffer.Writer.BaseStream.Position; 178 | 179 | // add CRC 180 | var crc = ModbusUtils.CalculateCRC(_frameBuffer.Buffer.AsMemory()[..frameLength]); 181 | _frameBuffer.Writer.Write(crc); 182 | frameLength = (int)_frameBuffer.Writer.BaseStream.Position; 183 | 184 | // send request 185 | _serialPort!.Value.Value.Write(_frameBuffer.Buffer, 0, frameLength); 186 | 187 | // special case: broadcast (only for write commands) 188 | if (unitIdentifier == 0) 189 | return _frameBuffer.Buffer.AsSpan(0, 0); 190 | 191 | // wait for and process response 192 | frameLength = 0; 193 | _frameBuffer.Reader.BaseStream.Seek(0, SeekOrigin.Begin); 194 | 195 | while (true) 196 | { 197 | frameLength += _serialPort!.Value.Value.Read(_frameBuffer.Buffer, frameLength, _frameBuffer.Buffer.Length - frameLength); 198 | 199 | if (ModbusUtils.DetectResponseFrame(unitIdentifier, _frameBuffer.Buffer.AsMemory()[..frameLength])) 200 | { 201 | break; 202 | } 203 | 204 | else 205 | { 206 | // reset length because one or more chunks of data were received and written to 207 | // the buffer, but no valid Modbus frame could be detected and now the buffer is full 208 | if (frameLength == _frameBuffer.Buffer.Length) 209 | frameLength = 0; 210 | } 211 | } 212 | 213 | _ = _frameBuffer.Reader.ReadByte(); 214 | var rawFunctionCode = _frameBuffer.Reader.ReadByte(); 215 | 216 | if (rawFunctionCode == (byte)ModbusFunctionCode.Error + (byte)functionCode) 217 | ProcessError(functionCode, (ModbusExceptionCode)_frameBuffer.Buffer[2]); 218 | 219 | else if (rawFunctionCode != (byte)functionCode) 220 | throw new ModbusException(ErrorMessage.ModbusClient_InvalidResponseFunctionCode); 221 | 222 | return _frameBuffer.Buffer.AsSpan(1, frameLength - 3); 223 | } 224 | 225 | #endregion 226 | 227 | #region IDisposable 228 | 229 | private bool _disposedValue; 230 | 231 | /// 232 | protected virtual void Dispose(bool disposing) 233 | { 234 | if (!_disposedValue) 235 | { 236 | if (disposing) 237 | { 238 | Close(); 239 | } 240 | 241 | _disposedValue = true; 242 | } 243 | } 244 | 245 | /// 246 | /// Disposes the current instance. 247 | /// 248 | public void Dispose() 249 | { 250 | Dispose(disposing: true); 251 | GC.SuppressFinalize(this); 252 | } 253 | 254 | #endregion 255 | } -------------------------------------------------------------------------------- /src/FluentModbus/Client/ModbusRtuClientAsync.cs: -------------------------------------------------------------------------------- 1 |  2 | /* This is automatically translated code. */ 3 | 4 | namespace FluentModbus; 5 | 6 | public partial class ModbusRtuClient 7 | { 8 | /// 9 | protected override async Task> TransceiveFrameAsync(byte unitIdentifier, ModbusFunctionCode functionCode, Action extendFrame, CancellationToken cancellationToken = default) 10 | { 11 | // WARNING: IF YOU EDIT THIS METHOD, REFLECT ALL CHANGES ALSO IN TransceiveFrameAsync! 12 | 13 | // build request 14 | if (!(0 <= unitIdentifier && unitIdentifier <= 247)) 15 | throw new ModbusException(ErrorMessage.ModbusClient_InvalidUnitIdentifier); 16 | 17 | // special case: broadcast (only for write commands) 18 | if (unitIdentifier == 0) 19 | { 20 | switch (functionCode) 21 | { 22 | case ModbusFunctionCode.WriteMultipleRegisters: 23 | case ModbusFunctionCode.WriteSingleCoil: 24 | case ModbusFunctionCode.WriteSingleRegister: 25 | case ModbusFunctionCode.WriteMultipleCoils: 26 | case ModbusFunctionCode.WriteFileRecord: 27 | case ModbusFunctionCode.MaskWriteRegister: 28 | break; 29 | default: 30 | throw new ModbusException(ErrorMessage.Modbus_InvalidUseOfBroadcast); 31 | } 32 | } 33 | 34 | _frameBuffer.Writer.Seek(0, SeekOrigin.Begin); 35 | _frameBuffer.Writer.Write(unitIdentifier); // 00 Unit Identifier 36 | extendFrame(_frameBuffer.Writer); 37 | 38 | var frameLength = (int)_frameBuffer.Writer.BaseStream.Position; 39 | 40 | // add CRC 41 | var crc = ModbusUtils.CalculateCRC(_frameBuffer.Buffer.AsMemory()[..frameLength]); 42 | _frameBuffer.Writer.Write(crc); 43 | frameLength = (int)_frameBuffer.Writer.BaseStream.Position; 44 | 45 | // send request 46 | await _serialPort!.Value.Value.WriteAsync(_frameBuffer.Buffer, 0, frameLength, cancellationToken).ConfigureAwait(false); 47 | 48 | // special case: broadcast (only for write commands) 49 | if (unitIdentifier == 0) 50 | return _frameBuffer.Buffer.AsMemory(0, 0); 51 | 52 | // wait for and process response 53 | frameLength = 0; 54 | _frameBuffer.Reader.BaseStream.Seek(0, SeekOrigin.Begin); 55 | 56 | while (true) 57 | { 58 | frameLength += await _serialPort!.Value.Value.ReadAsync(_frameBuffer.Buffer, frameLength, _frameBuffer.Buffer.Length - frameLength, cancellationToken).ConfigureAwait(false); 59 | 60 | if (ModbusUtils.DetectResponseFrame(unitIdentifier, _frameBuffer.Buffer.AsMemory()[..frameLength])) 61 | { 62 | break; 63 | } 64 | 65 | else 66 | { 67 | // reset length because one or more chunks of data were received and written to 68 | // the buffer, but no valid Modbus frame could be detected and now the buffer is full 69 | if (frameLength == _frameBuffer.Buffer.Length) 70 | frameLength = 0; 71 | } 72 | } 73 | 74 | _ = _frameBuffer.Reader.ReadByte(); 75 | var rawFunctionCode = _frameBuffer.Reader.ReadByte(); 76 | 77 | if (rawFunctionCode == (byte)ModbusFunctionCode.Error + (byte)functionCode) 78 | ProcessError(functionCode, (ModbusExceptionCode)_frameBuffer.Buffer[2]); 79 | 80 | else if (rawFunctionCode != (byte)functionCode) 81 | throw new ModbusException(ErrorMessage.ModbusClient_InvalidResponseFunctionCode); 82 | 83 | return _frameBuffer.Buffer.AsMemory(1, frameLength - 3); 84 | } 85 | } -------------------------------------------------------------------------------- /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.cs: -------------------------------------------------------------------------------- 1 | 2 | /* This is automatically translated code. */ 3 | 4 | namespace FluentModbus; 5 | 6 | public partial class ModbusRtuOverTcpClient 7 | { 8 | /// 9 | protected override async Task> TransceiveFrameAsync(byte unitIdentifier, ModbusFunctionCode functionCode, Action extendFrame, CancellationToken cancellationToken = default) 10 | { 11 | // WARNING: IF YOU EDIT THIS METHOD, REFLECT ALL CHANGES ALSO IN TransceiveFrameAsync! 12 | 13 | var frameBuffer = _frameBuffer; 14 | var writer = _frameBuffer.Writer; 15 | var reader = _frameBuffer.Reader; 16 | 17 | // build request 18 | if (!(0 <= unitIdentifier && unitIdentifier <= 247)) 19 | throw new ModbusException(ErrorMessage.ModbusClient_InvalidUnitIdentifier); 20 | 21 | // special case: broadcast (only for write commands) 22 | if (unitIdentifier == 0) 23 | { 24 | switch (functionCode) 25 | { 26 | case ModbusFunctionCode.WriteMultipleRegisters: 27 | case ModbusFunctionCode.WriteSingleCoil: 28 | case ModbusFunctionCode.WriteSingleRegister: 29 | case ModbusFunctionCode.WriteMultipleCoils: 30 | case ModbusFunctionCode.WriteFileRecord: 31 | case ModbusFunctionCode.MaskWriteRegister: 32 | break; 33 | default: 34 | throw new ModbusException(ErrorMessage.Modbus_InvalidUseOfBroadcast); 35 | } 36 | } 37 | 38 | writer.Seek(0, SeekOrigin.Begin); 39 | writer.Write(unitIdentifier); // 00 Unit Identifier 40 | extendFrame(writer); 41 | 42 | var frameLength = (int)writer.BaseStream.Position; 43 | 44 | // add CRC 45 | var crc = ModbusUtils.CalculateCRC(frameBuffer.Buffer.AsMemory()[..frameLength]); 46 | writer.Write(crc); 47 | frameLength = (int)writer.BaseStream.Position; 48 | 49 | // send request 50 | await _networkStream.WriteAsync(frameBuffer.Buffer, 0, frameLength, cancellationToken).ConfigureAwait(false); 51 | 52 | // special case: broadcast (only for write commands) 53 | if (unitIdentifier == 0) 54 | return _frameBuffer.Buffer.AsMemory(0, 0); 55 | 56 | // wait for and process response 57 | frameLength = 0; 58 | reader.BaseStream.Seek(0, SeekOrigin.Begin); 59 | 60 | while (true) 61 | { 62 | using var timeoutCts = new CancellationTokenSource(_networkStream.ReadTimeout); 63 | 64 | // https://stackoverflow.com/a/62162138 65 | // https://github.com/Apollo3zehn/FluentModbus/blob/181586d88cbbef3b2b3e6ace7b29099e04b30627/src/FluentModbus/ModbusRtuSerialPort.cs#L54 66 | using (timeoutCts.Token.Register(_networkStream.Close)) 67 | using (cancellationToken.Register(timeoutCts.Cancel)) 68 | { 69 | try 70 | { 71 | frameLength += await _networkStream.ReadAsync(frameBuffer.Buffer, frameLength, frameBuffer.Buffer.Length - frameLength, cancellationToken).ConfigureAwait(false); 72 | } 73 | catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) 74 | { 75 | throw; 76 | } 77 | catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested) 78 | { 79 | throw new TimeoutException("The asynchronous read operation timed out."); 80 | } 81 | catch (IOException) when (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested) 82 | { 83 | throw new TimeoutException("The asynchronous read operation timed out."); 84 | } 85 | } 86 | 87 | /* From MSDN (https://docs.microsoft.com/en-us/dotnet/api/system.io.stream.read): 88 | * Implementations of this method read a maximum of count bytes from the current stream and store 89 | * them in buffer beginning at offset. The current position within the stream is advanced by the 90 | * number of bytes read; however, if an exception occurs, the current position within the stream 91 | * remains unchanged. Implementations return the number of bytes read. The implementation will block 92 | * until at least one byte of data can be read, in the event that no data is available. Read returns 93 | * 0 only when there is no more data in the stream and no more is expected (such as a closed socket or end of file). 94 | * An implementation is free to return fewer bytes than requested even if the end of the stream has not been reached. 95 | */ 96 | 97 | if (ModbusUtils.DetectResponseFrame(unitIdentifier, _frameBuffer.Buffer.AsMemory()[..frameLength])) 98 | { 99 | break; 100 | } 101 | else 102 | { 103 | // reset length because one or more chunks of data were received and written to 104 | // the buffer, but no valid Modbus frame could be detected and now the buffer is full 105 | if (frameLength == _frameBuffer.Buffer.Length) 106 | frameLength = 0; 107 | } 108 | } 109 | 110 | _ = reader.ReadByte(); 111 | var rawFunctionCode = reader.ReadByte(); 112 | 113 | if (rawFunctionCode == (byte)ModbusFunctionCode.Error + (byte)functionCode) 114 | ProcessError(functionCode, (ModbusExceptionCode)_frameBuffer.Buffer[2]); 115 | 116 | else if (rawFunctionCode != (byte)functionCode) 117 | throw new ModbusException(ErrorMessage.ModbusClient_InvalidResponseFunctionCode); 118 | 119 | return _frameBuffer.Buffer.AsMemory(1, frameLength - 3); 120 | } 121 | } -------------------------------------------------------------------------------- /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.cs: -------------------------------------------------------------------------------- 1 |  2 | /* This is automatically translated code. */ 3 | 4 | namespace FluentModbus; 5 | 6 | public partial class ModbusTcpClient 7 | { 8 | /// 9 | protected override async Task> TransceiveFrameAsync(byte unitIdentifier, ModbusFunctionCode functionCode, Action extendFrame, CancellationToken cancellationToken = default) 10 | { 11 | // WARNING: IF YOU EDIT THIS METHOD, REFLECT ALL CHANGES ALSO IN TransceiveFrameAsync! 12 | 13 | ushort bytesFollowing = 0; 14 | 15 | var frameBuffer = _frameBuffer; 16 | var writer = _frameBuffer.Writer; 17 | var reader = _frameBuffer.Reader; 18 | 19 | // build request 20 | writer.Seek(7, SeekOrigin.Begin); 21 | extendFrame(writer); 22 | var frameLength = (int)writer.BaseStream.Position; 23 | 24 | writer.Seek(0, SeekOrigin.Begin); 25 | 26 | if (BitConverter.IsLittleEndian) 27 | { 28 | writer.WriteReverse(GetTransactionIdentifier()); // 00-01 Transaction Identifier 29 | writer.WriteReverse((ushort)0); // 02-03 Protocol Identifier 30 | writer.WriteReverse((ushort)(frameLength - 6)); // 04-05 Length 31 | } 32 | 33 | else 34 | { 35 | writer.Write(GetTransactionIdentifier()); // 00-01 Transaction Identifier 36 | writer.Write((ushort)0); // 02-03 Protocol Identifier 37 | writer.Write((ushort)(frameLength - 6)); // 04-05 Length 38 | } 39 | 40 | writer.Write(unitIdentifier); // 06 Unit Identifier 41 | 42 | // send request 43 | await _networkStream.WriteAsync(frameBuffer.Buffer, 0, frameLength, cancellationToken).ConfigureAwait(false); 44 | 45 | // wait for and process response 46 | frameLength = 0; 47 | var isParsed = false; 48 | reader.BaseStream.Seek(0, SeekOrigin.Begin); 49 | 50 | while (true) 51 | { 52 | int partialLength; 53 | 54 | using var timeoutCts = new CancellationTokenSource(_networkStream.ReadTimeout); 55 | 56 | // https://stackoverflow.com/a/62162138 57 | // https://github.com/Apollo3zehn/FluentModbus/blob/181586d88cbbef3b2b3e6ace7b29099e04b30627/src/FluentModbus/ModbusRtuSerialPort.cs#L54 58 | using (timeoutCts.Token.Register(_networkStream.Close)) 59 | using (cancellationToken.Register(timeoutCts.Cancel)) 60 | { 61 | try 62 | { 63 | partialLength = await _networkStream.ReadAsync(frameBuffer.Buffer, frameLength, frameBuffer.Buffer.Length - frameLength, cancellationToken).ConfigureAwait(false); 64 | } 65 | catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) 66 | { 67 | throw; 68 | } 69 | catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested) 70 | { 71 | throw new TimeoutException("The asynchronous read operation timed out."); 72 | } 73 | catch (IOException) when (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested) 74 | { 75 | throw new TimeoutException("The asynchronous read operation timed out."); 76 | } 77 | } 78 | 79 | /* From MSDN (https://docs.microsoft.com/en-us/dotnet/api/system.io.stream.read): 80 | * Implementations of this method read a maximum of count bytes from the current stream and store 81 | * them in buffer beginning at offset. The current position within the stream is advanced by the 82 | * number of bytes read; however, if an exception occurs, the current position within the stream 83 | * remains unchanged. Implementations return the number of bytes read. The implementation will block 84 | * until at least one byte of data can be read, in the event that no data is available. Read returns 85 | * 0 only when there is no more data in the stream and no more is expected (such as a closed socket or end of file). 86 | * An implementation is free to return fewer bytes than requested even if the end of the stream has not been reached. 87 | */ 88 | if (partialLength == 0) 89 | throw new InvalidOperationException(ErrorMessage.ModbusClient_TcpConnectionClosedUnexpectedly); 90 | 91 | frameLength += partialLength; 92 | 93 | if (frameLength >= 7) 94 | { 95 | if (!isParsed) // read MBAP header only once 96 | { 97 | // read MBAP header 98 | _ = reader.ReadUInt16Reverse(); // 00-01 Transaction Identifier 99 | var protocolIdentifier = reader.ReadUInt16Reverse(); // 02-03 Protocol Identifier 100 | bytesFollowing = reader.ReadUInt16Reverse(); // 04-05 Length 101 | _ = reader.ReadByte(); // 06 Unit Identifier 102 | 103 | if (protocolIdentifier != 0) 104 | throw new ModbusException(ErrorMessage.ModbusClient_InvalidProtocolIdentifier); 105 | 106 | isParsed = true; 107 | } 108 | 109 | // full frame received 110 | if (frameLength - 6 >= bytesFollowing) 111 | break; 112 | } 113 | } 114 | 115 | var rawFunctionCode = reader.ReadByte(); 116 | 117 | if (rawFunctionCode == (byte)ModbusFunctionCode.Error + (byte)functionCode) 118 | ProcessError(functionCode, (ModbusExceptionCode)frameBuffer.Buffer[8]); 119 | 120 | else if (rawFunctionCode != (byte)functionCode) 121 | throw new ModbusException(ErrorMessage.ModbusClient_InvalidResponseFunctionCode); 122 | 123 | return frameBuffer.Buffer.AsMemory(7, frameLength - 7); 124 | } 125 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/FluentModbus.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Lightweight and fast client and server implementation of the Modbus protocol (TCP/RTU, Sync/Async). 5 | Modbus ModbusTCP ModbusRTU .NET Standard Windows Linux 6 | true 7 | netstandard2.0;netstandard2.1 8 | icon.png 9 | README.md 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ModbusClientAsync.cs 40 | TextTemplatingFileGenerator 41 | 42 | 43 | ModbusRtuClientAsync.cs 44 | TextTemplatingFileGenerator 45 | 46 | 47 | ModbusRtuOverTcpClientAsync.cs 48 | TextTemplatingFileGenerator 49 | 50 | 51 | ModbusTcpClientAsync.cs 52 | TextTemplatingFileGenerator 53 | 54 | 55 | 56 | 57 | 58 | True 59 | True 60 | ModbusClientAsync.tt 61 | 62 | 63 | True 64 | True 65 | ModbusRtuOverTcpClientAsync.tt 66 | 67 | 68 | True 69 | True 70 | ModbusTcpClientAsync.tt 71 | 72 | 73 | True 74 | True 75 | ModbusRtuClientAsync.tt 76 | 77 | 78 | True 79 | True 80 | ErrorMessage.resx 81 | 82 | 83 | 84 | 85 | 86 | ResXFileCodeGenerator 87 | ErrorMessage.Designer.cs 88 | FluentModbus 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/FluentModbus/IModbusRtuSerialPort.cs: -------------------------------------------------------------------------------- 1 | namespace FluentModbus; 2 | 3 | /// 4 | /// A serial port for Modbus RTU communication. 5 | /// 6 | public interface IModbusRtuSerialPort 7 | { 8 | #region Methods 9 | 10 | /// 11 | /// Reads a number of bytes from the serial port input buffer and writes those bytes into a byte array at the specified offset. 12 | /// 13 | /// The byte array to write the input to. 14 | /// The offset in at which to write the bytes. 15 | /// The maximum number of bytes to read. Fewer bytes are read if is greater than the number of bytes in the input buffer. 16 | /// The number of bytes read. 17 | int Read(byte[] buffer, int offset, int count); 18 | 19 | /// 20 | /// Asynchronously reads a number of bytes from the serial port input buffer and writes those bytes into a byte array at the specified offset. 21 | /// 22 | /// The byte array to write the input to. 23 | /// The offset in at which to write the bytes. 24 | /// The maximum number of bytes to read. Fewer bytes are read if is greater than the number of bytes in the input buffer. 25 | /// A token to cancel the current operation. 26 | /// The number of bytes read. 27 | Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken token); 28 | 29 | /// 30 | /// Writes a specified number of bytes to the serial port using data from a buffer. 31 | /// 32 | /// The byte array that contains the data to write to the port. 33 | /// The zero-based byte offset in the parameter at which to begin copying bytes to the port. 34 | /// The number of bytes to write. 35 | void Write(byte[] buffer, int offset, int count); 36 | 37 | /// 38 | /// Asynchronously writes a specified number of bytes to the serial port using data from a buffer. 39 | /// 40 | /// The byte array that contains the data to write to the port. 41 | /// The zero-based byte offset in the parameter at which to begin copying bytes to the port. 42 | /// The number of bytes to write. 43 | /// A token to cancel the current operation. 44 | Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken token); 45 | 46 | /// 47 | /// Opens a new serial port connection. 48 | /// 49 | void Open(); 50 | 51 | /// 52 | /// Closes the port connection, sets the property to , and disposes of the internal Stream object. 53 | /// 54 | void Close(); 55 | 56 | #endregion Methods 57 | 58 | #region Properties 59 | 60 | /// 61 | /// Gets the port for communications, including but not limited to all available COM ports. 62 | /// 63 | string PortName { get; } 64 | 65 | /// 66 | /// Gets a value indicating the open or closed status of the serial port object. 67 | /// 68 | bool IsOpen { get; } 69 | 70 | #endregion Properties 71 | } -------------------------------------------------------------------------------- /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 { } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/FluentModbus/ModbusRtuSerialPort.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Ports; 2 | 3 | namespace FluentModbus; 4 | 5 | /// 6 | /// A wrapper for a . 7 | /// 8 | public class ModbusRtuSerialPort : IModbusRtuSerialPort 9 | { 10 | #region Fields 11 | 12 | private readonly SerialPort _serialPort; 13 | 14 | #endregion 15 | 16 | #region Constructors 17 | 18 | /// 19 | /// Initializes a new instances of the class. 20 | /// 21 | /// The serial port to wrap. 22 | public ModbusRtuSerialPort(SerialPort serialPort) 23 | { 24 | _serialPort = serialPort; 25 | } 26 | 27 | #endregion 28 | 29 | #region Properties 30 | 31 | /// 32 | /// Gets the port for communications. 33 | /// 34 | public string PortName => _serialPort.PortName; 35 | 36 | /// 37 | /// Gets a value indicating the open or closed status of the object. 38 | /// 39 | public bool IsOpen => _serialPort.IsOpen; 40 | 41 | #endregion 42 | 43 | #region Methods 44 | 45 | /// 46 | /// Opens a new serial port connection. 47 | /// 48 | public void Open() 49 | { 50 | _serialPort.Open(); 51 | } 52 | 53 | /// 54 | /// Closes the port connection, sets the property to , and disposes of the internal object. 55 | /// 56 | public void Close() 57 | { 58 | _serialPort.Close(); 59 | } 60 | 61 | /// 62 | /// Reads from the input buffer. 63 | /// 64 | /// The byte array to write the input to. 65 | /// The offset in at which to write the bytes. 66 | /// The maximum number of bytes to read. Fewer bytes are read if is greater than the number of bytes in the input buffer. 67 | /// The number of bytes read. 68 | public int Read(byte[] buffer, int offset, int count) 69 | { 70 | return _serialPort.Read(buffer, offset, count); 71 | } 72 | 73 | /// 74 | /// Asynchronously reads from the input buffer. 75 | /// 76 | /// The byte array to write the input to. 77 | /// The offset in at which to write the bytes. 78 | /// The maximum number of bytes to read. Fewer bytes are read if is greater than the number of bytes in the input buffer. 79 | /// A token to cancel the current operation. 80 | /// The number of bytes read. 81 | public async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken token) 82 | { 83 | // https://github.com/AndreasAmMueller/Modbus/blob/a6d11080c2f5a1205681c881f3ba163d2ac84a1f/src/Modbus.Serial/Util/Extensions.cs#L69 84 | // https://stackoverflow.com/a/54610437/11906695 85 | // https://github.com/dotnet/runtime/issues/28968 86 | 87 | using var timeoutCts = new CancellationTokenSource(_serialPort.ReadTimeout); 88 | 89 | /* _serialPort.DiscardInBuffer is essential here to cancel the operation */ 90 | using (timeoutCts.Token.Register(() => 91 | { 92 | if (IsOpen) 93 | _serialPort.DiscardInBuffer(); 94 | })) 95 | using (token.Register(() => timeoutCts.Cancel())) 96 | { 97 | try 98 | { 99 | return await _serialPort.BaseStream.ReadAsync(buffer, offset, count, timeoutCts.Token); 100 | } 101 | catch (OperationCanceledException) when (token.IsCancellationRequested) 102 | { 103 | throw; 104 | } 105 | catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested) 106 | { 107 | throw new TimeoutException("The asynchronous read operation timed out."); 108 | } 109 | catch (IOException) when (timeoutCts.IsCancellationRequested && !token.IsCancellationRequested) 110 | { 111 | throw new TimeoutException("The asynchronous read operation timed out."); 112 | } 113 | } 114 | } 115 | 116 | /// 117 | /// Writes data to the serial port output buffer. 118 | /// 119 | /// The byte array that contains the data to write to the port. 120 | /// The zero-based byte offset in the parameter at which to begin copying bytes to the port. 121 | /// The number of bytes to write. 122 | public void Write(byte[] buffer, int offset, int count) 123 | { 124 | _serialPort.Write(buffer, offset, count); 125 | } 126 | 127 | /// 128 | /// Asynchronously writes data to the serial port output buffer. 129 | /// 130 | /// The byte array that contains the data to write to the port. 131 | /// The zero-based byte offset in the parameter at which to begin copying bytes to the port. 132 | /// The number of bytes to write. 133 | /// A token to cancel the current operation. 134 | public async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken token) 135 | { 136 | // https://github.com/AndreasAmMueller/Modbus/blob/a6d11080c2f5a1205681c881f3ba163d2ac84a1f/src/Modbus.Serial/Util/Extensions.cs#L69 137 | // https://stackoverflow.com/a/54610437/11906695 138 | // https://github.com/dotnet/runtime/issues/28968 139 | using var timeoutCts = new CancellationTokenSource(_serialPort.WriteTimeout); 140 | 141 | /* _serialPort.DiscardInBuffer is essential here to cancel the operation */ 142 | using (timeoutCts.Token.Register(() => _serialPort.DiscardOutBuffer())) 143 | using (token.Register(() => timeoutCts.Cancel())) 144 | { 145 | try 146 | { 147 | await _serialPort.BaseStream.WriteAsync(buffer, offset, count, timeoutCts.Token); 148 | } 149 | catch (OperationCanceledException) when (token.IsCancellationRequested) 150 | { 151 | throw; 152 | } 153 | catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested) 154 | { 155 | throw new TimeoutException("The asynchronous write operation timed out."); 156 | } 157 | catch (IOException) when (timeoutCts.IsCancellationRequested && !token.IsCancellationRequested) 158 | { 159 | throw new TimeoutException("The asynchronous write operation timed out."); 160 | } 161 | } 162 | } 163 | 164 | #endregion 165 | } 166 | -------------------------------------------------------------------------------- /src/FluentModbus/ModbusUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Net; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | 6 | #if NETSTANDARD2_1_OR_GREATER 7 | using System.Diagnostics.CodeAnalysis; 8 | #endif 9 | 10 | namespace FluentModbus; 11 | 12 | internal static class ModbusUtils 13 | { 14 | #if NETSTANDARD2_0 15 | public static bool TryParseEndpoint(ReadOnlySpan value, out IPEndPoint? result) 16 | #endif 17 | #if NETSTANDARD2_1_OR_GREATER 18 | public static bool TryParseEndpoint(ReadOnlySpan value, [NotNullWhen(true)] out IPEndPoint? result) 19 | #endif 20 | { 21 | var addressLength = value.Length; 22 | var lastColonPos = value.LastIndexOf(':'); 23 | 24 | if (lastColonPos > 0) 25 | { 26 | if (value[lastColonPos - 1] == ']') 27 | addressLength = lastColonPos; 28 | 29 | else if (value[..lastColonPos].LastIndexOf(':') == -1) 30 | addressLength = lastColonPos; 31 | } 32 | 33 | if (IPAddress.TryParse(value[..addressLength].ToString(), out var address)) 34 | { 35 | var port = 502U; 36 | 37 | if (addressLength == value.Length || 38 | (uint.TryParse(value[(addressLength + 1)..].ToString(), NumberStyles.None, CultureInfo.InvariantCulture, out port) && port <= 65536)) 39 | 40 | { 41 | result = new IPEndPoint(address, (int)port); 42 | return true; 43 | } 44 | } 45 | 46 | result = default; 47 | 48 | return false; 49 | } 50 | 51 | public static ushort CalculateCRC(Memory buffer) 52 | { 53 | var span = buffer.Span; 54 | ushort crc = 0xFFFF; 55 | 56 | foreach (var value in span) 57 | { 58 | crc ^= value; 59 | 60 | for (int i = 0; i < 8; i++) 61 | { 62 | if ((crc & 0x0001) != 0) 63 | { 64 | crc >>= 1; 65 | crc ^= 0xA001; 66 | } 67 | else 68 | { 69 | crc >>= 1; 70 | } 71 | } 72 | } 73 | 74 | return crc; 75 | } 76 | 77 | public static bool DetectRequestFrame(byte unitIdentifier, Memory frame) 78 | { 79 | #warning This method should be improved by validating the total length against the expected length depending on the function code 80 | /* Correct response frame (min. 4 bytes) 81 | * 00 Unit Identifier 82 | * 01 Function Code 83 | * (0..x bytes - depends on function code) 84 | * n-1 CRC Byte 1 85 | * n CRC Byte 2 86 | */ 87 | 88 | var span = frame.Span; 89 | 90 | if (span.Length < 4) 91 | return false; 92 | 93 | if (unitIdentifier != 255) // 255 means "skip unit identifier check" 94 | { 95 | var newUnitIdentifier = span[0]; 96 | 97 | if (newUnitIdentifier != unitIdentifier) 98 | return false; 99 | } 100 | 101 | // CRC check 102 | var crcBytes = span.Slice(span.Length - 2, 2); 103 | var actualCRC = unchecked((ushort)((crcBytes[1] << 8) + crcBytes[0])); 104 | var expectedCRC = CalculateCRC(frame[..^2]); 105 | 106 | if (actualCRC != expectedCRC) 107 | return false; 108 | 109 | return true; 110 | } 111 | 112 | public static bool DetectResponseFrame(byte unitIdentifier, Memory frame) 113 | { 114 | // 115 | 116 | /* Response frame for read methods (0x01, 0x02, 0x03, 0x04, 0x17) (min. 6 bytes) 117 | * 00 Unit Identifier 118 | * 01 Function Code 119 | * 02 Byte count 120 | * 03 Minimum of 1 byte 121 | * 04 CRC Byte 1 122 | * 05 CRC Byte 2 123 | */ 124 | 125 | /* Response frame for write methods (0x05, 0x06, 0x0F, 0x10) (8 bytes) 126 | * 00 Unit Identifier 127 | * 01 Function Code 128 | * 02 Address 129 | * 03 Address 130 | * 04 Value 131 | * 05 Value 132 | * 06 CRC Byte 1 133 | * 07 CRC Byte 2 134 | */ 135 | 136 | /* Error response frame (5 bytes) 137 | * 00 Unit Identifier 138 | * 01 Function Code + 0x80 139 | * 02 Exception Code 140 | * 03 CRC Byte 1 141 | * 04 CRC Byte 2 142 | */ 143 | 144 | var span = frame.Span; 145 | 146 | // absolute minimum frame size 147 | if (span.Length < 5) 148 | return false; 149 | 150 | // 255 means "skip unit identifier check" 151 | if (unitIdentifier != 255) 152 | { 153 | var newUnitIdentifier = span[0]; 154 | 155 | if (newUnitIdentifier != unitIdentifier) 156 | return false; 157 | } 158 | 159 | // Byte count check 160 | if (span[1] < 0x80) 161 | { 162 | switch (span[1]) 163 | { 164 | // Read methods 165 | case 0x01: 166 | case 0x02: 167 | case 0x03: 168 | case 0x04: 169 | case 0x17: 170 | 171 | if (span.Length < span[2] + 5) 172 | return false; 173 | 174 | break; 175 | 176 | // Write methods 177 | case 0x05: 178 | case 0x06: 179 | case 0x0F: 180 | case 0x10: 181 | 182 | if (span.Length < 8) 183 | return false; 184 | 185 | break; 186 | } 187 | } 188 | 189 | // Error (only for completeness, length >= 5 has already been checked above) 190 | else 191 | { 192 | if (span.Length < 5) 193 | return false; 194 | } 195 | 196 | // CRC check 197 | var crcBytes = span.Slice(span.Length - 2, 2); 198 | var actualCRC = unchecked((ushort)((crcBytes[1] << 8) + crcBytes[0])); 199 | var expectedCRC = CalculateCRC(frame[..^2]); 200 | 201 | if (actualCRC != expectedCRC) 202 | return false; 203 | 204 | return true; 205 | } 206 | 207 | public static short SwitchEndianness(short value) 208 | { 209 | var bytes = BitConverter.GetBytes(value); 210 | return (short)((bytes[0] << 8) + bytes[1]); 211 | } 212 | 213 | public static ushort SwitchEndianness(ushort value) 214 | { 215 | var bytes = BitConverter.GetBytes(value); 216 | return (ushort)((bytes[0] << 8) + bytes[1]); 217 | } 218 | 219 | public static T SwitchEndianness(T value) where T : unmanaged 220 | { 221 | Span data = stackalloc T[] { value }; 222 | SwitchEndianness(data); 223 | 224 | return data[0]; 225 | } 226 | 227 | public static T ConvertBetweenLittleEndianAndMidLittleEndian(T value) where T : unmanaged 228 | { 229 | // from DCBA to CDAB 230 | 231 | if (Unsafe.SizeOf() == 4) 232 | { 233 | Span data = stackalloc T[] { value, default }; 234 | 235 | var dataset_bytes = MemoryMarshal.Cast(data); 236 | var offset = 4; 237 | 238 | dataset_bytes[offset + 0] = dataset_bytes[1]; 239 | dataset_bytes[offset + 1] = dataset_bytes[0]; 240 | dataset_bytes[offset + 2] = dataset_bytes[3]; 241 | dataset_bytes[offset + 3] = dataset_bytes[2]; 242 | 243 | return data[1]; 244 | } 245 | else 246 | { 247 | throw new Exception($"Type {value.GetType().Name} cannot be represented as mid-little-endian."); 248 | } 249 | } 250 | 251 | public static void SwitchEndianness(Memory dataset) where T : unmanaged 252 | { 253 | SwitchEndianness(dataset.Span); 254 | } 255 | 256 | public static void SwitchEndianness(Span dataset) where T : unmanaged 257 | { 258 | var size = Marshal.SizeOf(); 259 | var dataset_bytes = MemoryMarshal.Cast(dataset); 260 | 261 | for (int i = 0; i < dataset_bytes.Length; i += size) 262 | { 263 | for (int j = 0; j < size / 2; j++) 264 | { 265 | var i1 = i + j; 266 | var i2 = i - j + size - 1; 267 | 268 | (dataset_bytes[i2], dataset_bytes[i1]) = (dataset_bytes[i1], dataset_bytes[i2]); 269 | } 270 | } 271 | } 272 | } -------------------------------------------------------------------------------- /src/FluentModbus/Resources/ErrorMessage.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | The function code received in the query is not an allowable action for the server. This may be because the function code is only applicable to newer devices, and was not implemented in the unit selected. It could also indicate that the server is in the wrong state to process a request of this type, for example because it is unconfigured and is being asked to return register values. 122 | 123 | 124 | The data address received in the query is not an allowable address for the server. More specifically, the combination of reference number and transfer length is invalid. 125 | 126 | 127 | A value contained in the query data field is not an allowable value for server. This indicates a fault in the structure of the remainder of a complex request, such as that the implied length is incorrect. 128 | 129 | 130 | The quantity of registers is out of range (1..123). Make sure to request a minimum of one register. If you use the generic overload methods, please note that a single register consists of 2 bytes. If, for example, 1 x int32 value is requested, this results in a read operation of 2 registers. 131 | 132 | 133 | The quantity of registers is out of range (1..125). Make sure to request a minimum of one register. If you use the generic overload methods, please note that a single register consists of 2 bytes. If, for example, 1 x int32 value is requested, this results in a read operation of 2 registers. 134 | 135 | 136 | The quantity of coils is out of range (1..2000). 137 | 138 | 139 | An unrecoverable error occurred while the server was attempting to perform the requested action. 140 | 141 | 142 | The server has accepted the request and is processing it, but a long duration of time will be required to do so. 143 | 144 | 145 | The server is engaged in processing a long–duration program command. 146 | 147 | 148 | The server attempted to read record file, but detected a parity error in the memory. 149 | 150 | 151 | The gateway was unable to allocate an internal communication path from the input port to the output port for processing the request. 152 | 153 | 154 | No response was obtained from the target device 155 | 156 | 157 | Array length must be equal to two bytes. 158 | 159 | 160 | Array length must be greater than two bytes and even. 161 | 162 | 163 | The exception code received from the server is invalid. 164 | 165 | 166 | The protocol identifier is invalid. 167 | 168 | 169 | The responsed function code is invalid. 170 | 171 | 172 | The response message length is invalid. 173 | 174 | 175 | The unit identifier is invalid. Valid node addresses are in the range of 0 - 247. Use address '0' to broadcast write command to all available servers. 176 | 177 | 178 | Quantity must be a positive integer number. Choose the 'count' parameter such that an even number of bytes is requested. 179 | 180 | 181 | The TCP connection closed unexpectedly. 182 | 183 | 184 | Could not connect within the specified time. 185 | 186 | 187 | Unknown {0} Modbus error received. 188 | 189 | 190 | The unit identifier is invalid. Valid node addresses are in the range of 1 - 247. 191 | 192 | 193 | No unit found for the specified unit identifier. 194 | 195 | 196 | There is no valid request available. 197 | 198 | 199 | Invalid use of broadcast: Unit identifier '0' can only be used for write operations. 200 | 201 | 202 | The value is invalid. Valid values are in the range of 0 - 65535. 203 | 204 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /src/FluentModbus/Server/ModbusRtuRequestHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace FluentModbus; 4 | 5 | internal class ModbusRtuRequestHandler : ModbusRequestHandler, IDisposable 6 | { 7 | #region Fields 8 | 9 | private IModbusRtuSerialPort _serialPort; 10 | 11 | private readonly ILogger _logger; 12 | 13 | #endregion 14 | 15 | #region Constructors 16 | 17 | public ModbusRtuRequestHandler(IModbusRtuSerialPort serialPort, ModbusRtuServer rtuServer, ILogger logger) : base(rtuServer, 256) 18 | { 19 | _logger = logger; 20 | _serialPort = serialPort; 21 | _serialPort.Open(); 22 | 23 | base.Start(); 24 | } 25 | 26 | #endregion 27 | 28 | #region Properties 29 | 30 | public override string DisplayName => _serialPort.PortName; 31 | 32 | protected override bool IsResponseRequired => ModbusServer.UnitIdentifiers.Contains(UnitIdentifier); 33 | 34 | #endregion 35 | 36 | #region Methods 37 | 38 | internal override async Task ReceiveRequestAsync() 39 | { 40 | if (CancellationToken.IsCancellationRequested) 41 | return; 42 | 43 | IsReady = false; 44 | 45 | try 46 | { 47 | if (await TryReceiveRequestAsync()) 48 | { 49 | IsReady = true; // WriteResponse() can be called only when IsReady = true 50 | 51 | if (ModbusServer.IsAsynchronous) 52 | WriteResponse(); 53 | } 54 | } 55 | catch (Exception ex) 56 | { 57 | _logger.LogDebug(ex, "The connection will be closed"); 58 | 59 | CancelToken(); 60 | } 61 | } 62 | 63 | protected override int WriteFrame(Action extendFrame) 64 | { 65 | int frameLength; 66 | ushort crc; 67 | 68 | FrameBuffer.Writer.Seek(0, SeekOrigin.Begin); 69 | 70 | // add unit identifier 71 | FrameBuffer.Writer.Write(UnitIdentifier); 72 | 73 | // add PDU 74 | extendFrame(); 75 | 76 | // add CRC 77 | frameLength = unchecked((int)FrameBuffer.Writer.BaseStream.Position); 78 | crc = ModbusUtils.CalculateCRC(FrameBuffer.Buffer.AsMemory(0, frameLength)); 79 | FrameBuffer.Writer.Write(crc); 80 | 81 | return frameLength + 2; 82 | } 83 | 84 | protected override void OnResponseReady(int frameLength) 85 | { 86 | _serialPort.Write(FrameBuffer.Buffer, 0, frameLength); 87 | } 88 | 89 | private async Task TryReceiveRequestAsync() 90 | { 91 | // Whenever the serial port has a read timeout set, a TimeoutException might 92 | // occur which is catched immediately. The reason is that - opposed to the TCP 93 | // server - the RTU server maintains only a single connection and if that 94 | // connection is closed, there would be no point in running that server anymore. 95 | // To avoid that, the connection is kept alive by catching the TimeoutException. 96 | 97 | Length = 0; 98 | 99 | try 100 | { 101 | while (true) 102 | { 103 | Length += await _serialPort.ReadAsync(FrameBuffer.Buffer, Length, FrameBuffer.Buffer.Length - Length, CancellationToken); 104 | 105 | // full frame received 106 | if (ModbusUtils.DetectRequestFrame(255, FrameBuffer.Buffer.AsMemory(0, Length))) 107 | { 108 | FrameBuffer.Reader.BaseStream.Seek(0, SeekOrigin.Begin); 109 | 110 | // read unit identifier 111 | UnitIdentifier = FrameBuffer.Reader.ReadByte(); 112 | 113 | break; 114 | } 115 | else 116 | { 117 | // reset length because one or more chunks of data were received and written to 118 | // the buffer, but no valid Modbus frame could be detected and now the buffer is full 119 | if (Length == FrameBuffer.Buffer.Length) 120 | Length = 0; 121 | } 122 | } 123 | } 124 | catch (TimeoutException) 125 | { 126 | return false; 127 | } 128 | 129 | // make sure that the incoming frame is actually adressed to this server 130 | if (ModbusServer.UnitIdentifiers.Contains(UnitIdentifier)) 131 | { 132 | LastRequest.Restart(); 133 | return true; 134 | } 135 | 136 | else 137 | { 138 | return false; 139 | } 140 | } 141 | 142 | #endregion 143 | 144 | #region IDisposable Support 145 | 146 | private bool _disposedValue = false; 147 | 148 | protected override void Dispose(bool disposing) 149 | { 150 | if (!_disposedValue) 151 | { 152 | if (disposing) 153 | _serialPort.Close(); 154 | 155 | _disposedValue = true; 156 | } 157 | 158 | base.Dispose(disposing); 159 | } 160 | 161 | #endregion 162 | } -------------------------------------------------------------------------------- /src/FluentModbus/Server/ModbusRtuServer.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Ports; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Logging.Abstractions; 4 | 5 | namespace FluentModbus; 6 | 7 | /// 8 | /// A Modbus RTU server. 9 | /// 10 | public class ModbusRtuServer : ModbusServer 11 | { 12 | #region Fields 13 | 14 | private IModbusRtuSerialPort? _serialPort; 15 | 16 | #endregion 17 | 18 | #region Constructors 19 | 20 | /// 21 | /// Creates a Modbus RTU server with support for holding registers (read and write, 16 bit), input registers (read-only, 16 bit), coils (read and write, 1 bit) and discete inputs (read-only, 1 bit). 22 | /// 23 | /// Enables or disables the asynchronous operation, where each client request is processed immediately using a locking mechanism. Use synchronuous operation to avoid locks in the hosting application. See the documentation for more details. 24 | /// The unique Modbus RTU unit identifier (1..247). 25 | public ModbusRtuServer(byte unitIdentifier, bool isAsynchronous = true) 26 | : this(logger: NullLogger.Instance, unitIdentifier, isAsynchronous) 27 | { 28 | // 29 | } 30 | 31 | /// 32 | /// Creates a Modbus RTU server with support for holding registers (read and write, 16 bit), input registers (read-only, 16 bit), coils (read and write, 1 bit) and discete inputs (read-only, 1 bit). 33 | /// 34 | /// The logger to use. 35 | /// Enables or disables the asynchronous operation, where each client request is processed immediately using a locking mechanism. Use synchronuous operation to avoid locks in the hosting application. See the documentation for more details. 36 | /// The unique Modbus RTU unit identifier (1..247). 37 | public ModbusRtuServer(ILogger logger, byte unitIdentifier, bool isAsynchronous = true) : base(isAsynchronous, logger) 38 | { 39 | AddUnit(unitIdentifier); 40 | } 41 | 42 | /// 43 | /// Creates a multi-unit Modbus RTU server with support for holding registers (read and write, 16 bit), input registers (read-only, 16 bit), coils (read and write, 1 bit) and discete inputs (read-only, 1 bit). 44 | /// 45 | /// Enables or disables the asynchronous operation, where each client request is processed immediately using a locking mechanism. Use synchronuous operation to avoid locks in the hosting application. See the documentation for more details. 46 | /// The unique Modbus RTU unit identifiers (1..247). 47 | public ModbusRtuServer(IEnumerable unitIdentifiers, bool isAsynchronous = true) 48 | : this(logger: NullLogger.Instance, unitIdentifiers, isAsynchronous) 49 | { 50 | // 51 | } 52 | 53 | /// 54 | /// Creates a multi-unit Modbus RTU server with support for holding registers (read and write, 16 bit), input registers (read-only, 16 bit), coils (read and write, 1 bit) and discete inputs (read-only, 1 bit). 55 | /// 56 | /// The logger to use. 57 | /// Enables or disables the asynchronous operation, where each client request is processed immediately using a locking mechanism. Use synchronuous operation to avoid locks in the hosting application. See the documentation for more details. 58 | /// The unique Modbus RTU unit identifiers (1..247). 59 | public ModbusRtuServer(ILogger logger, IEnumerable unitIdentifiers, bool isAsynchronous = true) : base(isAsynchronous, logger) 60 | { 61 | foreach (var unitIdentifier in unitIdentifiers) 62 | { 63 | AddUnit(unitIdentifier); 64 | } 65 | } 66 | 67 | #endregion 68 | 69 | #region Properties 70 | 71 | /// 72 | /// Gets the connection status of the underlying serial port. 73 | /// 74 | public bool IsConnected 75 | { 76 | get 77 | { 78 | return 79 | _serialPort is not null && 80 | _serialPort.IsOpen; 81 | } 82 | } 83 | 84 | /// 85 | /// Gets or sets the serial baud rate. Default is 9600. 86 | /// 87 | public int BaudRate { get; set; } = 9600; 88 | 89 | /// 90 | /// Gets or sets the handshaking protocol for serial port transmission of data. Default is Handshake.None. 91 | /// 92 | public Handshake Handshake { get; set; } = Handshake.None; 93 | 94 | /// 95 | /// Gets or sets the parity-checking protocol. Default is Parity.Even. 96 | /// 97 | public Parity Parity { get; set; } = Parity.Even; 98 | 99 | /// 100 | /// Gets or sets the standard number of stopbits per byte. Default is StopBits.One. 101 | /// 102 | public StopBits StopBits { get; set; } = StopBits.One; 103 | 104 | /// 105 | /// Gets or sets the read timeout in milliseconds. Default is 1000 ms. 106 | /// 107 | public int ReadTimeout { get; set; } = 1000; 108 | 109 | /// 110 | /// Gets or sets the write timeout in milliseconds. Default is 1000 ms. 111 | /// 112 | public int WriteTimeout { get; set; } = 1000; 113 | 114 | internal ModbusRtuRequestHandler? RequestHandler { get; private set; } 115 | 116 | #endregion 117 | 118 | #region Methods 119 | 120 | /// 121 | /// Starts the server. It will listen on the provided . 122 | /// 123 | /// The COM port to be used, e.g. COM1. 124 | public void Start(string port) 125 | { 126 | IModbusRtuSerialPort serialPort = new ModbusRtuSerialPort(new SerialPort(port) 127 | { 128 | BaudRate = BaudRate, 129 | Handshake = Handshake, 130 | Parity = Parity, 131 | StopBits = StopBits, 132 | ReadTimeout = ReadTimeout, 133 | WriteTimeout = WriteTimeout 134 | }); 135 | 136 | _serialPort = serialPort; 137 | 138 | Start(serialPort); 139 | } 140 | 141 | /// 142 | /// Starts the server. It will communicate using the provided . 143 | /// 144 | /// The serial port to be used. 145 | public void Start(IModbusRtuSerialPort serialPort) 146 | { 147 | /* According to the spec (https://www.modbus.org/docs/Modbus_over_serial_line_V1_02.pdf), 148 | * section 2.5.1 RTU Transmission Mode: "... the use of no parity requires 2 stop bits." 149 | * Remove this check to improve compatibility (#56). 150 | */ 151 | 152 | //if (Parity == Parity.None && StopBits != StopBits.Two) 153 | // throw new InvalidOperationException(ErrorMessage.Modbus_NoParityRequiresTwoStopBits); 154 | 155 | base.StopProcessing(); 156 | base.StartProcessing(); 157 | 158 | RequestHandler = new ModbusRtuRequestHandler(serialPort, this, Logger); 159 | 160 | // remove clients asynchronously 161 | /* https://stackoverflow.com/questions/2782802/can-net-task-instances-go-out-of-scope-during-run */ 162 | Task.Run(async () => 163 | { 164 | while (!CTS.IsCancellationRequested) 165 | { 166 | lock (Lock) 167 | { 168 | if (// This condition may become true if an external SerialPort is used 169 | // and the user set a custom read timeout. 170 | // This should be the only cause but since "ReceiveRequestAsync" is never 171 | // awaited, the actual cause may be different. 172 | RequestHandler.CancellationToken.IsCancellationRequested) 173 | { 174 | StopProcessing(); 175 | } 176 | } 177 | 178 | await Task.Delay(TimeSpan.FromSeconds(1)); 179 | } 180 | }, CTS.Token); 181 | } 182 | 183 | /// 184 | /// Stops the server and closes the underlying serial port. 185 | /// 186 | public override void Stop() 187 | { 188 | base.StopProcessing(); 189 | 190 | RequestHandler?.Dispose(); 191 | } 192 | /// 193 | /// Dynamically adds a new unit to the server. 194 | /// 195 | /// The identifier of the unit to add. 196 | public new void AddUnit(byte unitIdentifier) 197 | { 198 | if (0 < unitIdentifier && unitIdentifier <= 247) 199 | base.AddUnit(unitIdentifier); 200 | 201 | else 202 | throw new ArgumentException(ErrorMessage.ModbusServer_InvalidUnitIdentifier); 203 | } 204 | 205 | /// 206 | protected override void ProcessRequests() 207 | { 208 | lock (Lock) 209 | { 210 | if (RequestHandler is not null && RequestHandler.IsReady) 211 | { 212 | if (RequestHandler.Length > 0) 213 | RequestHandler.WriteResponse(); 214 | 215 | _ = RequestHandler.ReceiveRequestAsync(); 216 | } 217 | } 218 | } 219 | 220 | #endregion 221 | } -------------------------------------------------------------------------------- /src/FluentModbus/Server/ModbusTcpRequestHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Sockets; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace FluentModbus; 6 | 7 | internal class ModbusTcpRequestHandler : ModbusRequestHandler, IDisposable 8 | { 9 | #region Fields 10 | 11 | private readonly TcpClient _tcpClient; 12 | private readonly NetworkStream _networkStream; 13 | 14 | private ushort _transactionIdentifier; 15 | private ushort _protocolIdentifier; 16 | private ushort _bytesFollowing; 17 | 18 | private readonly ILogger _logger; 19 | 20 | #endregion 21 | 22 | #region Constructors 23 | 24 | public ModbusTcpRequestHandler(TcpClient tcpClient, ModbusTcpServer tcpServer, ILogger logger) 25 | : base(tcpServer, 260) 26 | { 27 | _logger = logger; 28 | _tcpClient = tcpClient; 29 | _networkStream = tcpClient.GetStream(); 30 | 31 | DisplayName = ((IPEndPoint)_tcpClient.Client.RemoteEndPoint).Address.ToString(); 32 | CancellationToken.Register(() => _networkStream.Close()); 33 | 34 | base.Start(); 35 | } 36 | 37 | #endregion 38 | 39 | #region Properties 40 | 41 | public override string DisplayName { get; } 42 | 43 | protected override bool IsResponseRequired => true; 44 | 45 | #endregion 46 | 47 | #region Methods 48 | 49 | internal override async Task ReceiveRequestAsync() 50 | { 51 | if (CancellationToken.IsCancellationRequested) 52 | return; 53 | 54 | IsReady = false; 55 | 56 | try 57 | { 58 | if (await TryReceiveRequestAsync()) 59 | { 60 | IsReady = true; // WriteResponse() can be called only when IsReady = true 61 | 62 | if (ModbusServer.IsAsynchronous) 63 | WriteResponse(); 64 | } 65 | } 66 | catch (Exception ex) 67 | { 68 | _logger.LogDebug(ex, "The connection will be closed"); 69 | 70 | CancelToken(); 71 | } 72 | } 73 | 74 | protected override int WriteFrame(Action extendFrame) 75 | { 76 | int length; 77 | 78 | FrameBuffer.Writer.Seek(7, SeekOrigin.Begin); 79 | 80 | // add PDU 81 | extendFrame.Invoke(); 82 | 83 | // add MBAP 84 | length = (int)FrameBuffer.Writer.BaseStream.Position; 85 | FrameBuffer.Writer.Seek(0, SeekOrigin.Begin); 86 | 87 | if (BitConverter.IsLittleEndian) 88 | { 89 | FrameBuffer.Writer.WriteReverse(_transactionIdentifier); 90 | FrameBuffer.Writer.WriteReverse(_protocolIdentifier); 91 | FrameBuffer.Writer.WriteReverse((byte)(length - 6)); 92 | } 93 | else 94 | { 95 | FrameBuffer.Writer.Write(_transactionIdentifier); 96 | FrameBuffer.Writer.Write(_protocolIdentifier); 97 | FrameBuffer.Writer.Write((byte)(length - 6)); 98 | } 99 | 100 | FrameBuffer.Writer.Write(UnitIdentifier); 101 | 102 | return length; 103 | } 104 | 105 | protected override void OnResponseReady(int frameLength) 106 | { 107 | _networkStream.Write(FrameBuffer.Buffer, 0, frameLength); 108 | } 109 | 110 | private async Task TryReceiveRequestAsync() 111 | { 112 | // Whenever the network stream has a read timeout set, a TimeoutException 113 | // might occur which is catched later in ReceiveRequestAsync() where the token is 114 | // cancelled. Up to 1 second later, the connection clean up method detects that the 115 | // token has been cancelled and removes the client from the list of connectected 116 | // clients. 117 | 118 | int partialLength; 119 | bool isParsed; 120 | 121 | isParsed = false; 122 | 123 | Length = 0; 124 | _bytesFollowing = 0; 125 | 126 | while (true) 127 | { 128 | if (_networkStream.DataAvailable) 129 | { 130 | partialLength = _networkStream.Read(FrameBuffer.Buffer, 0, FrameBuffer.Buffer.Length); 131 | } 132 | else 133 | { 134 | // actually, CancellationToken is ignored - therefore: CancellationToken.Register(() => ...); 135 | partialLength = await _networkStream.ReadAsync(FrameBuffer.Buffer, 0, FrameBuffer.Buffer.Length, CancellationToken); 136 | } 137 | 138 | if (partialLength > 0) 139 | { 140 | Length += partialLength; 141 | 142 | if (Length >= 7) 143 | { 144 | if (!isParsed) // read MBAP header only once 145 | { 146 | FrameBuffer.Reader.BaseStream.Seek(0, SeekOrigin.Begin); 147 | 148 | // read MBAP header 149 | _transactionIdentifier = FrameBuffer.Reader.ReadUInt16Reverse(); // 00-01 Transaction Identifier 150 | _protocolIdentifier = FrameBuffer.Reader.ReadUInt16Reverse(); // 02-03 Protocol Identifier 151 | _bytesFollowing = FrameBuffer.Reader.ReadUInt16Reverse(); // 04-05 Length 152 | UnitIdentifier = FrameBuffer.Reader.ReadByte(); // 06 Unit Identifier 153 | 154 | if (_protocolIdentifier != 0) 155 | { 156 | Length = 0; 157 | break; 158 | } 159 | 160 | isParsed = true; 161 | } 162 | 163 | // full frame received 164 | if (Length - 6 >= _bytesFollowing) 165 | { 166 | LastRequest.Restart(); 167 | break; 168 | } 169 | } 170 | } 171 | else 172 | { 173 | Length = 0; 174 | break; 175 | } 176 | } 177 | 178 | // Make sure that the incoming frame is actually addressed to this server. 179 | // If we have only one UnitIdentifier, and it is zero, then we accept all 180 | // incoming messages 181 | if (ModbusServer.IsSingleZeroUnitMode || ModbusServer.UnitIdentifiers.Contains(UnitIdentifier)) 182 | { 183 | LastRequest.Restart(); 184 | return true; 185 | } 186 | 187 | else 188 | { 189 | return false; 190 | } 191 | } 192 | 193 | #endregion 194 | 195 | #region IDisposable Support 196 | 197 | private bool _disposedValue = false; 198 | 199 | protected override void Dispose(bool disposing) 200 | { 201 | if (!_disposedValue) 202 | { 203 | if (disposing) 204 | _tcpClient.Close(); 205 | 206 | _disposedValue = true; 207 | } 208 | 209 | base.Dispose(disposing); 210 | } 211 | 212 | #endregion 213 | } -------------------------------------------------------------------------------- /src/FluentModbus/Server/ModbusTcpServer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Microsoft.Extensions.Logging.Abstractions; 3 | using System.Net; 4 | using System.Net.Sockets; 5 | 6 | namespace FluentModbus; 7 | 8 | /// 9 | /// A Modbus TCP server. 10 | /// 11 | public class ModbusTcpServer : ModbusServer 12 | { 13 | #region Fields 14 | 15 | private bool _leaveOpen; 16 | private ITcpClientProvider? _tcpClientProvider; 17 | 18 | #endregion 19 | 20 | #region Constructors 21 | 22 | /// 23 | /// Creates a Modbus TCP server with support for holding registers (read and write, 16 bit), input registers (read-only, 16 bit), coils (read and write, 1 bit) and discete inputs (read-only, 1 bit). 24 | /// 25 | /// Enables or disables the asynchronous operation, where each client request is processed immediately using a locking mechanism. Use synchronuous operation to avoid locks in the hosting application. See the documentation for more details. 26 | public ModbusTcpServer(bool isAsynchronous = true) : base(isAsynchronous, logger: NullLogger.Instance) 27 | { 28 | AddUnit(0); 29 | } 30 | 31 | /// 32 | /// Creates a Modbus TCP server with support for holding registers (read and write, 16 bit), input registers (read-only, 16 bit), coils (read and write, 1 bit) and discete inputs (read-only, 1 bit). 33 | /// 34 | /// The logger to use. 35 | /// Enables or disables the asynchronous operation, where each client request is processed immediately using a locking mechanism. Use synchronuous operation to avoid locks in the hosting application. See the documentation for more details. 36 | public ModbusTcpServer(ILogger logger, bool isAsynchronous = true) : base(isAsynchronous, logger) 37 | { 38 | AddUnit(0); 39 | } 40 | 41 | #endregion 42 | 43 | #region Properties 44 | 45 | /// 46 | /// Gets or sets the timeout for each client connection. When the client does not send any request within the specified period of time, the connection will be reset. Default is 1 minute. 47 | /// 48 | public TimeSpan ConnectionTimeout { get; set; } = DefaultConnectionTimeout; 49 | 50 | /// 51 | /// Gets or sets the maximum number of concurrent client connections. A value of zero means there is no limit. 52 | /// 53 | public int MaxConnections { get; set; } = 0; 54 | 55 | /// 56 | /// Gets the number of currently connected clients. 57 | /// 58 | public int ConnectionCount => RequestHandlers.Count; 59 | 60 | internal static TimeSpan DefaultConnectionTimeout { get; set; } = TimeSpan.FromMinutes(1); 61 | 62 | internal List RequestHandlers { get; private set; } = new List(); 63 | 64 | #endregion 65 | 66 | #region Methods 67 | 68 | /// 69 | /// Starts the server. It will listen on any IP address on port 502. 70 | /// 71 | public void Start() 72 | { 73 | Start(new IPEndPoint(IPAddress.Any, 502)); 74 | } 75 | 76 | /// 77 | /// Starts the server. It will listen on the provided on port 502. 78 | /// 79 | public void Start(IPAddress ipAddress) 80 | { 81 | Start(new IPEndPoint(ipAddress, 502)); 82 | } 83 | 84 | /// 85 | /// Starts the server. It will listen on the provided . 86 | /// 87 | public void Start(IPEndPoint localEndpoint) 88 | { 89 | Start(new DefaultTcpClientProvider(localEndpoint)); 90 | } 91 | 92 | /// 93 | /// Starts the server. It will accept all TCP clients provided by the provided . 94 | /// 95 | /// The TCP client provider. 96 | /// to leave the TCP client provider open after the object is stopped or disposed; otherwise, . 97 | public void Start(ITcpClientProvider tcpClientProvider, bool leaveOpen = false) 98 | { 99 | _tcpClientProvider = tcpClientProvider; 100 | _leaveOpen = leaveOpen; 101 | 102 | base.StopProcessing(); 103 | base.StartProcessing(); 104 | 105 | RequestHandlers = new List(); 106 | 107 | // accept clients asynchronously 108 | /* https://stackoverflow.com/questions/2782802/can-net-task-instances-go-out-of-scope-during-run */ 109 | Task.Run(async () => 110 | { 111 | while (!CTS.IsCancellationRequested) 112 | { 113 | // There are no default timeouts (SendTimeout and ReceiveTimeout = 0), 114 | // use ConnectionTimeout instead. 115 | var tcpClient = await _tcpClientProvider.AcceptTcpClientAsync(); 116 | var requestHandler = new ModbusTcpRequestHandler(tcpClient, this, Logger); 117 | 118 | lock (Lock) 119 | { 120 | if (MaxConnections > 0 && 121 | /* request handler is added later in 'else' block, so count needs to be increased by 1 */ 122 | RequestHandlers.Count + 1 > MaxConnections) 123 | { 124 | tcpClient.Close(); 125 | } 126 | 127 | else 128 | { 129 | RequestHandlers.Add(requestHandler); 130 | Logger.LogInformation($"{RequestHandlers.Count} {(RequestHandlers.Count == 1 ? "client is" : "clients are")} connected"); 131 | } 132 | } 133 | } 134 | }, CTS.Token); 135 | 136 | // remove clients asynchronously 137 | /* https://stackoverflow.com/questions/2782802/can-net-task-instances-go-out-of-scope-during-run */ 138 | Task.Run(async () => 139 | { 140 | while (!CTS.IsCancellationRequested) 141 | { 142 | lock (Lock) 143 | { 144 | // see remarks to "TcpClient.Connected" property 145 | // https://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.tcpclient.connected?redirectedfrom=MSDN&view=netframework-4.8#System_Net_Sockets_TcpClient_Connected 146 | foreach (var requestHandler in RequestHandlers.ToList()) 147 | { 148 | if (// This condition may become true if an external TcpClientProvider is used 149 | // and the user set a custom read timeout on the provided TcpClient. 150 | // This should be the only cause but since "ReceiveRequestAsync" is never 151 | // awaited, the actual cause may be different. 152 | requestHandler.CancellationToken.IsCancellationRequested || 153 | // or there was no request received within the specified timeout 154 | requestHandler.LastRequest.Elapsed > ConnectionTimeout) 155 | { 156 | try 157 | { 158 | if (requestHandler.CancellationToken.IsCancellationRequested) 159 | Logger.LogInformation($"Connection {requestHandler.DisplayName} was closed due to an error"); 160 | 161 | else 162 | Logger.LogInformation($"Connection {requestHandler.DisplayName} timed out"); 163 | 164 | // remove request handler 165 | RequestHandlers.Remove(requestHandler); 166 | Logger.LogInformation($"{RequestHandlers.Count} {(RequestHandlers.Count == 1 ? "client is" : "clients are")} connected"); 167 | 168 | requestHandler.Dispose(); 169 | } 170 | catch 171 | { 172 | // ignore error 173 | } 174 | } 175 | } 176 | } 177 | 178 | await Task.Delay(TimeSpan.FromSeconds(1)); 179 | } 180 | }, CTS.Token); 181 | } 182 | 183 | /// 184 | /// Starts the server. It will use only the provided . 185 | /// 186 | /// The TCP client to communicate with. 187 | public void Start(TcpClient tcpClient) 188 | { 189 | base.StopProcessing(); 190 | base.StartProcessing(); 191 | 192 | RequestHandlers = new List() 193 | { 194 | new ModbusTcpRequestHandler(tcpClient, this, Logger) 195 | }; 196 | } 197 | 198 | /// 199 | /// Stops the server and closes all open TCP connections. 200 | /// 201 | public override void Stop() 202 | { 203 | base.StopProcessing(); 204 | 205 | if (!_leaveOpen) 206 | _tcpClientProvider?.Dispose(); 207 | 208 | RequestHandlers.ForEach(requestHandler => requestHandler.Dispose()); 209 | } 210 | 211 | /// 212 | protected override void ProcessRequests() 213 | { 214 | lock (Lock) 215 | { 216 | foreach (var requestHandler in RequestHandlers) 217 | { 218 | if (requestHandler.IsReady) 219 | { 220 | if (requestHandler.Length > 0) 221 | requestHandler.WriteResponse(); 222 | 223 | _ = requestHandler.ReceiveRequestAsync(); 224 | } 225 | } 226 | } 227 | } 228 | 229 | #endregion 230 | } -------------------------------------------------------------------------------- /src/FluentModbus/SpanExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace FluentModbus; 5 | 6 | /// 7 | /// Contains extension methods to read and write data from the Modbus registers. 8 | /// 9 | public static class SpanExtensions 10 | { 11 | /// 12 | /// Writes a single value of type to the registers and converts it to the little-endian representation if necessary. 13 | /// 14 | /// The type of the value to write. 15 | /// The target buffer. 16 | /// The Modbus register address. 17 | /// The value to write. 18 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 19 | public static void SetLittleEndian(this Span buffer, int address, T value) 20 | where T : unmanaged 21 | { 22 | // DCBA 23 | if (!(0 <= address && address <= ushort.MaxValue)) 24 | throw new Exception(ErrorMessage.Modbus_InvalidValueUShort); 25 | 26 | var byteBuffer = MemoryMarshal 27 | .AsBytes(buffer) 28 | .Slice(address * 2); 29 | 30 | if (!BitConverter.IsLittleEndian) 31 | value = ModbusUtils.SwitchEndianness(value); 32 | 33 | Unsafe.WriteUnaligned(ref byteBuffer.GetPinnableReference(), value); 34 | } 35 | 36 | /// 37 | /// Writes a single value of type to the registers and converts it to the mid-little-endian representation. 38 | /// 39 | /// The type of the value to write. 40 | /// The target buffer. 41 | /// The Modbus register address. 42 | /// The value to write. 43 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 44 | public static void SetMidLittleEndian(this Span buffer, int address, T value) 45 | where T : unmanaged 46 | { 47 | // CDAB 48 | if (!(0 <= address && address <= ushort.MaxValue)) 49 | throw new Exception(ErrorMessage.Modbus_InvalidValueUShort); 50 | 51 | var byteBuffer = MemoryMarshal 52 | .AsBytes(buffer) 53 | .Slice(address * 2); 54 | 55 | if (!BitConverter.IsLittleEndian) 56 | value = ModbusUtils.SwitchEndianness(value); 57 | 58 | value = ModbusUtils.ConvertBetweenLittleEndianAndMidLittleEndian(value); 59 | 60 | Unsafe.WriteUnaligned(ref byteBuffer.GetPinnableReference(), value); 61 | } 62 | 63 | /// 64 | /// Writes a single value of type to the registers and converts it to the big-endian representation if necessary. 65 | /// 66 | /// The type of the value to write. 67 | /// The target buffer. 68 | /// The Modbus register address. 69 | /// The value to write. 70 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 71 | public static void SetBigEndian(this Span buffer, int address, T value) 72 | where T : unmanaged 73 | { 74 | // ABCD 75 | if (!(0 <= address && address <= ushort.MaxValue)) 76 | throw new Exception(ErrorMessage.Modbus_InvalidValueUShort); 77 | 78 | var byteBuffer = MemoryMarshal 79 | .AsBytes(buffer) 80 | .Slice(address * 2); 81 | 82 | if (BitConverter.IsLittleEndian) 83 | value = ModbusUtils.SwitchEndianness(value); 84 | 85 | Unsafe.WriteUnaligned(ref byteBuffer.GetPinnableReference(), value); 86 | } 87 | 88 | /// 89 | /// Reads a single little-endian value of type from the registers. 90 | /// 91 | /// The type of the value to read. 92 | /// The source buffer. 93 | /// The Modbus register address. 94 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 95 | public static T GetLittleEndian(this Span buffer, int address) 96 | where T : unmanaged 97 | { 98 | if (!(0 <= address && address <= ushort.MaxValue)) 99 | throw new Exception(ErrorMessage.Modbus_InvalidValueUShort); 100 | 101 | var byteBuffer = MemoryMarshal 102 | .AsBytes(buffer) 103 | .Slice(address * 2); 104 | 105 | var value = Unsafe.ReadUnaligned(ref byteBuffer.GetPinnableReference()); 106 | 107 | if (!BitConverter.IsLittleEndian) 108 | value = ModbusUtils.SwitchEndianness(value); 109 | 110 | return value; 111 | } 112 | 113 | /// 114 | /// Reads a single mid-little-endian value of type from the registers. 115 | /// 116 | /// The type of the value to read. 117 | /// The source buffer. 118 | /// The Modbus register address. 119 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 120 | public static T GetMidLittleEndian(this Span buffer, int address) 121 | where T : unmanaged 122 | { 123 | if (!(0 <= address && address <= ushort.MaxValue)) 124 | throw new Exception(ErrorMessage.Modbus_InvalidValueUShort); 125 | 126 | var byteBuffer = MemoryMarshal 127 | .AsBytes(buffer) 128 | .Slice(address * 2); 129 | 130 | var value = Unsafe.ReadUnaligned(ref byteBuffer.GetPinnableReference()); 131 | value = ModbusUtils.ConvertBetweenLittleEndianAndMidLittleEndian(value); 132 | 133 | if (!BitConverter.IsLittleEndian) 134 | value = ModbusUtils.SwitchEndianness(value); 135 | 136 | return value; 137 | } 138 | 139 | /// 140 | /// Reads a single big-endian value of type from the registers. 141 | /// 142 | /// The type of the value to read. 143 | /// The source buffer. 144 | /// The Modbus register address. 145 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 146 | public static T GetBigEndian(this Span buffer, int address) 147 | where T : unmanaged 148 | { 149 | if (!(0 <= address && address <= ushort.MaxValue)) 150 | throw new Exception(ErrorMessage.Modbus_InvalidValueUShort); 151 | 152 | var byteBuffer = MemoryMarshal 153 | .AsBytes(buffer) 154 | .Slice(address * 2); 155 | 156 | var value = Unsafe.ReadUnaligned(ref byteBuffer.GetPinnableReference()); 157 | 158 | if (BitConverter.IsLittleEndian) 159 | value = ModbusUtils.SwitchEndianness(value); 160 | 161 | return value; 162 | } 163 | 164 | /// 165 | /// Writes a single bit to the buffer. 166 | /// 167 | /// The target buffer. 168 | /// The Modbus address. 169 | /// The value to set. 170 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 171 | public static void Set(this Span buffer, int address, bool value) 172 | { 173 | if (!(0 <= address && address <= ushort.MaxValue)) 174 | throw new Exception(ErrorMessage.Modbus_InvalidValueUShort); 175 | 176 | var byteIndex = address / 8; 177 | var bitIndex = address % 8; 178 | 179 | // set 180 | if (value) 181 | buffer[byteIndex] |= (byte)(1 << bitIndex); 182 | 183 | // clear 184 | else 185 | buffer[byteIndex] &= (byte)~(1 << bitIndex); 186 | } 187 | 188 | /// 189 | /// Reads a single bit from the buffer. 190 | /// 191 | /// The source buffer. 192 | /// The Modbus address. 193 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 194 | public static bool Get(this Span buffer, int address) 195 | { 196 | if (!(0 <= address && address <= ushort.MaxValue)) 197 | throw new Exception(ErrorMessage.Modbus_InvalidValueUShort); 198 | 199 | var byteIndex = address / 8; 200 | var bitIndex = address % 8; 201 | var value = (buffer[byteIndex] & (1 << bitIndex)) > 0; 202 | 203 | return value; 204 | } 205 | 206 | /// 207 | /// Toggles a single bit in the buffer. 208 | /// 209 | /// The source buffer. 210 | /// The Modbus address. 211 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 212 | public static void Toggle(this Span buffer, int address) 213 | { 214 | if (!(0 <= address && address <= ushort.MaxValue)) 215 | throw new Exception(ErrorMessage.Modbus_InvalidValueUShort); 216 | 217 | var byteIndex = address / 8; 218 | var bitIndex = address % 8; 219 | 220 | buffer[byteIndex] ^= (byte)(1 << bitIndex); 221 | } 222 | 223 | /// 224 | /// Casts a memory of one primitive type to a memory of another primitive type. 225 | /// 226 | /// The type of the source memory. 227 | /// The type of the target memory. 228 | /// The source slice to convert. 229 | /// The converted memory. 230 | public static Memory Cast(this Memory memory) 231 | where TFrom : struct 232 | where TTo : struct 233 | { 234 | // avoid the extra allocation/indirection, at the cost of a gen-0 box 235 | if (typeof(TFrom) == typeof(TTo)) 236 | return (Memory)(object)memory; 237 | 238 | return new CastMemoryManager(memory).Memory; 239 | } 240 | } -------------------------------------------------------------------------------- /tests/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | VSTHRD200 7 | 8 | 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/ModbusTcpServerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Net.Sockets; 3 | using Xunit; 4 | using Xunit.Abstractions; 5 | 6 | namespace FluentModbus.Tests; 7 | 8 | public class ModbusTcpServerTests : IClassFixture 9 | { 10 | private readonly ITestOutputHelper _logger; 11 | 12 | public ModbusTcpServerTests(ITestOutputHelper logger) 13 | { 14 | _logger = logger; 15 | } 16 | 17 | [Fact] 18 | public async void ServerWorksWithExternalTcpClient() 19 | { 20 | // Arrange 21 | var endpoint = EndpointSource.GetNext(); 22 | var listener = new TcpListener(endpoint); 23 | using var server = new ModbusTcpServer(); 24 | var client = new ModbusTcpClient(); 25 | 26 | var expected = new double[] { 0.1, 0.2, 0.3, 0.4, 0.5 }; 27 | double[]? actual = default; 28 | 29 | // Act 30 | listener.Start(); 31 | 32 | var clientTask = Task.Run(() => 33 | { 34 | client.Connect(endpoint); 35 | 36 | actual = client 37 | .ReadWriteMultipleRegisters(0, 0, 5, 0, expected) 38 | .ToArray(); 39 | 40 | client.Disconnect(); 41 | }); 42 | 43 | var serverTask = Task.Run(() => 44 | { 45 | var tcpClient = listener.AcceptTcpClient(); 46 | server.Start(tcpClient); 47 | }); 48 | 49 | await clientTask; 50 | 51 | // Assert 52 | Assert.True(actual!.SequenceEqual(expected)); 53 | } 54 | 55 | [Fact(Skip = "Test depends on machine power.")] 56 | public async void ServerHandlesMultipleClients() 57 | { 58 | // Arrange 59 | var endpoint = EndpointSource.GetNext(); 60 | 61 | using var server = new ModbusTcpServer(); 62 | server.Start(endpoint); 63 | 64 | // Act 65 | var clients = Enumerable.Range(0, 20).Select(index => new ModbusTcpClient()).ToList(); 66 | 67 | var tasks = clients.Select((client, index) => 68 | { 69 | var data = Enumerable.Range(0, 20).Select(i => (float)i).ToArray(); 70 | 71 | client.Connect(endpoint); 72 | _logger.WriteLine($"Client {index}: Connected."); 73 | 74 | return Task.Run(async () => 75 | { 76 | _logger.WriteLine($"Client {index}: Task started."); 77 | 78 | for (int i = 0; i < 10; i++) 79 | { 80 | client.ReadHoldingRegisters(0, 0, 100); 81 | _logger.WriteLine($"Client {index}: ReadHoldingRegisters({i})"); 82 | await Task.Delay(50); 83 | client.WriteMultipleRegisters(0, 0, data); 84 | _logger.WriteLine($"Client {index}: WriteMultipleRegisters({i})"); 85 | await Task.Delay(50); 86 | client.ReadCoils(0, 0, 25); 87 | _logger.WriteLine($"Client {index}: ReadCoils({i})"); 88 | await Task.Delay(50); 89 | client.ReadInputRegisters(0, 0, 20); 90 | _logger.WriteLine($"Client {index}: ReadInputRegisters({i})"); 91 | await Task.Delay(50); 92 | } 93 | 94 | client.Disconnect(); 95 | }); 96 | }).ToList(); 97 | 98 | await Task.WhenAll(tasks); 99 | 100 | // Assert 101 | } 102 | 103 | [Fact] 104 | public async void ServerHandlesRequestFire() 105 | { 106 | // Arrange 107 | var endpoint = EndpointSource.GetNext(); 108 | 109 | using var server = new ModbusTcpServer(); 110 | server.Start(endpoint); 111 | 112 | // Act 113 | var client = new ModbusTcpClient(); 114 | client.Connect(endpoint); 115 | 116 | await Task.Run(() => 117 | { 118 | var data = Enumerable.Range(0, 20).Select(i => (float)i).ToArray(); 119 | var sw = Stopwatch.StartNew(); 120 | var iterations = 10000; 121 | 122 | for (int i = 0; i < iterations; i++) 123 | { 124 | client.WriteMultipleRegisters(0, 0, data); 125 | } 126 | 127 | var timePerRequest = sw.Elapsed.TotalMilliseconds / iterations; 128 | _logger.WriteLine($"Time per request: {timePerRequest * 1000:F0} us. Frequency: {1 / timePerRequest * 1000:F0} requests per second."); 129 | 130 | client.Disconnect(); 131 | }); 132 | 133 | // Assert 134 | } 135 | 136 | [Fact] 137 | public async void ServerRespectsMaxClientConnectionLimit() 138 | { 139 | // Arrange 140 | var endpoint = EndpointSource.GetNext(); 141 | 142 | using var server = new ModbusTcpServer() 143 | { 144 | MaxConnections = 2 145 | }; 146 | 147 | server.Start(endpoint); 148 | 149 | // Act 150 | var client1 = new ModbusTcpClient(); 151 | var client2 = new ModbusTcpClient(); 152 | var client3 = new ModbusTcpClient(); 153 | 154 | await Task.Run(() => 155 | { 156 | client1.Connect(endpoint); 157 | client1.WriteSingleRegister(0, 2, 3); 158 | 159 | client2.Connect(endpoint); 160 | client2.WriteSingleRegister(0, 2, 3); 161 | 162 | client3.Connect(endpoint); 163 | 164 | try 165 | { 166 | client3.WriteSingleRegister(0, 2, 3); 167 | throw new Exception("Modbus TCP server accepts too many clients."); 168 | } 169 | 170 | // Windows 171 | catch (IOException) { } 172 | 173 | // Linux 174 | catch (InvalidOperationException) { } 175 | 176 | server.MaxConnections = 3; 177 | 178 | client3.Connect(endpoint); 179 | client3.WriteSingleRegister(0, 2, 3); 180 | }); 181 | 182 | // Assert 183 | } 184 | } -------------------------------------------------------------------------------- /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 GenericTestData => new List 69 | { 70 | new object[] { new byte[] { 0x80, 0x90 }, new byte[] { 0x80, 0x90 } }, 71 | new object[] { new short[] { 0x6040, 0x6141 }, new short[] { 0x4060, 0x4161 } }, 72 | new object[] { new int[] { 0x60403020, 0x61413121 }, new int[] { 0x20304060, 0x21314161 } }, 73 | new object[] { new long[] { 0x6040302010203040, 0x6141312111213141 }, new long[] { 0x4030201020304060, 0x4131211121314161 } }, 74 | new object[] { new float[] { 0x60403020, 0x61413121 }, new float[] { 7.422001E+19F, 1.20603893E+21F } }, 75 | new object[] { new double[] { 0x6040302010203040, 0x6141312111213141 }, new double[] { 1.0482134659314621E-250, 3.0464944316161389E+59 } } 76 | }; 77 | 78 | [Theory] 79 | [MemberData(nameof(ModbusUtilsTests.GenericTestData))] 80 | public void SwapsEndiannessGeneric(T[] dataset, T[] expected) where T : unmanaged 81 | { 82 | // Act 83 | ModbusUtils.SwitchEndianness(dataset.AsSpan()); 84 | var actual = dataset; 85 | 86 | // Assert 87 | Assert.Equal(expected, actual); 88 | } 89 | 90 | [Fact] 91 | public void SwapsEndiannessMidLittleEndian() 92 | { 93 | // Arrange 94 | var data = (uint)0x01020304; 95 | 96 | // Act 97 | var expected = (uint)0x02010403; 98 | var actual1 = ModbusUtils.ConvertBetweenLittleEndianAndMidLittleEndian(data); 99 | var actual2 = ModbusUtils.ConvertBetweenLittleEndianAndMidLittleEndian(actual1); 100 | 101 | // Assert 102 | Assert.Equal(expected, actual1); 103 | Assert.Equal(data, actual2); 104 | } 105 | } -------------------------------------------------------------------------------- /tests/FluentModbus.Tests/ProtocolTestsAsync.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace FluentModbus.Tests; 4 | 5 | public class ProtocolTestsAsync : IClassFixture 6 | { 7 | private float[] _array; 8 | 9 | public ProtocolTestsAsync() 10 | { 11 | _array = [0, 0, 0, 0, 0, 65.455F, 24, 25, 0, 0]; 12 | } 13 | 14 | // FC03: ReadHoldingRegisters 15 | [Fact] 16 | public async Task FC03Test() 17 | { 18 | // Arrange 19 | var endpoint = EndpointSource.GetNext(); 20 | 21 | using var server = new ModbusTcpServer(); 22 | server.Start(endpoint); 23 | 24 | void AsyncWorkaround() 25 | { 26 | var buffer = server.GetHoldingRegisterBuffer(); 27 | 28 | buffer[6] = 65.455F; 29 | buffer[7] = 24; 30 | buffer[8] = 25; 31 | } 32 | 33 | lock (server.Lock) 34 | { 35 | AsyncWorkaround(); 36 | } 37 | 38 | var client = new ModbusTcpClient(); 39 | client.Connect(endpoint); 40 | 41 | // Act 42 | var actual = await client.ReadHoldingRegistersAsync(0, 2, 10); 43 | 44 | // Assert 45 | var expected = _array; 46 | 47 | Assert.True(expected.SequenceEqual(actual.ToArray())); 48 | } 49 | 50 | // FC16: WriteMultipleRegisters 51 | [Fact] 52 | public async Task FC16Test() 53 | { 54 | // Arrange 55 | var endpoint = EndpointSource.GetNext(); 56 | 57 | using var server = new ModbusTcpServer(); 58 | server.Start(endpoint); 59 | 60 | var client = new ModbusTcpClient(); 61 | client.Connect(endpoint); 62 | 63 | // Act 64 | await client.WriteMultipleRegistersAsync(0, 2, _array); 65 | 66 | // Assert 67 | var expected = _array; 68 | 69 | lock (server.Lock) 70 | { 71 | var actual = server.GetHoldingRegisterBuffer().Slice(1, 10).ToArray(); 72 | Assert.True(expected.SequenceEqual(actual)); 73 | } 74 | } 75 | 76 | // FC01: ReadCoils 77 | [Fact] 78 | public async Task FC01Test() 79 | { 80 | // Arrange 81 | var endpoint = EndpointSource.GetNext(); 82 | 83 | using var server = new ModbusTcpServer(); 84 | server.Start(endpoint); 85 | 86 | void AsyncWorkaround() 87 | { 88 | var buffer = server.GetCoilBuffer(); 89 | 90 | buffer[1] = 9; 91 | buffer[2] = 0; 92 | buffer[3] = 24; 93 | } 94 | 95 | lock (server.Lock) 96 | { 97 | AsyncWorkaround(); 98 | } 99 | 100 | var client = new ModbusTcpClient(); 101 | client.Connect(endpoint); 102 | 103 | // Act 104 | var actual = await client.ReadCoilsAsync(0, 8, 25); 105 | 106 | // Assert 107 | var expected = new byte[] { 9, 0, 24, 0 }; 108 | 109 | Assert.True(expected.SequenceEqual(actual.ToArray())); 110 | } 111 | 112 | // FC02: ReadDiscreteInputs 113 | [Fact] 114 | public async Task FC02Test() 115 | { 116 | // Arrange 117 | var endpoint = EndpointSource.GetNext(); 118 | 119 | using var server = new ModbusTcpServer(); 120 | server.Start(endpoint); 121 | 122 | void AsyncWorkaround() 123 | { 124 | var buffer = server.GetDiscreteInputBuffer(); 125 | 126 | buffer[1] = 9; 127 | buffer[2] = 0; 128 | buffer[3] = 24; 129 | } 130 | 131 | lock (server.Lock) 132 | { 133 | AsyncWorkaround(); 134 | } 135 | 136 | var client = new ModbusTcpClient(); 137 | client.Connect(endpoint); 138 | 139 | // Act 140 | var actual = await client.ReadDiscreteInputsAsync(0, 8, 25); 141 | 142 | // Assert 143 | var expected = new byte[] { 9, 0, 24, 0 }; 144 | 145 | Assert.True(expected.SequenceEqual(actual.ToArray())); 146 | } 147 | 148 | // FC04: ReadInputRegisters 149 | [Fact] 150 | public async Task FC04Test() 151 | { 152 | // Arrange 153 | var endpoint = EndpointSource.GetNext(); 154 | 155 | using var server = new ModbusTcpServer(); 156 | server.Start(endpoint); 157 | 158 | void AsyncWorkaround() 159 | { 160 | var buffer = server.GetInputRegisterBuffer(); 161 | 162 | buffer[6] = 65.455F; 163 | buffer[7] = 24; 164 | buffer[8] = 25; 165 | } 166 | 167 | lock (server.Lock) 168 | { 169 | AsyncWorkaround(); 170 | } 171 | 172 | var client = new ModbusTcpClient(); 173 | client.Connect(endpoint); 174 | 175 | // Act 176 | var actual = await client.ReadInputRegistersAsync(0, 2, 10); 177 | 178 | // Assert 179 | var expected = _array; 180 | 181 | Assert.True(expected.SequenceEqual(actual.ToArray())); 182 | } 183 | 184 | // FC05: WriteSingleCoil 185 | [Fact] 186 | public async Task FC05Test() 187 | { 188 | // Arrange 189 | var endpoint = EndpointSource.GetNext(); 190 | 191 | using var server = new ModbusTcpServer(); 192 | server.Start(endpoint); 193 | 194 | var client = new ModbusTcpClient(); 195 | client.Connect(endpoint); 196 | 197 | // Act 198 | await client.WriteSingleCoilAsync(0, 2, true); 199 | await client.WriteSingleCoilAsync(0, 7, true); 200 | await client.WriteSingleCoilAsync(0, 9, true); 201 | await client.WriteSingleCoilAsync(0, 26, true); 202 | 203 | // Assert 204 | var expected = new byte[] { 132, 2, 0, 4 }; 205 | 206 | lock (server.Lock) 207 | { 208 | var actual = server.GetCoilBuffer().Slice(0, 4).ToArray(); 209 | Assert.True(expected.SequenceEqual(actual)); 210 | } 211 | } 212 | 213 | // FC06: WriteSingleRegister 214 | [Fact] 215 | public async Task FC06Test() 216 | { 217 | // Arrange 218 | var endpoint = EndpointSource.GetNext(); 219 | 220 | using var server = new ModbusTcpServer(); 221 | server.Start(endpoint); 222 | 223 | var client = new ModbusTcpClient(); 224 | client.Connect(endpoint); 225 | 226 | // Act 227 | await client.WriteSingleRegisterAsync(0, 02, 259); 228 | await client.WriteSingleRegisterAsync(0, 10, 125); 229 | await client.WriteSingleRegisterAsync(0, 11, 16544); 230 | await client.WriteSingleRegisterAsync(0, 12, 4848); 231 | 232 | // Assert 233 | var expected = new short[] { 0, 0, 259, 0, 0, 0, 0, 0, 0, 0, 125, 16544, 4848 }; 234 | 235 | lock (server.Lock) 236 | { 237 | var actual = server.GetHoldingRegisterBuffer().Slice(0, 13).ToArray(); 238 | Assert.True(expected.SequenceEqual(actual)); 239 | } 240 | } 241 | 242 | // F023 ReadWriteMultipleRegisters 243 | [Fact] 244 | public async Task FC023Test() 245 | { 246 | // Arrange 247 | var endpoint = EndpointSource.GetNext(); 248 | 249 | using var server = new ModbusTcpServer(); 250 | server.Start(endpoint); 251 | 252 | void AsyncWorkaround() 253 | { 254 | var buffer = server.GetHoldingRegisterBuffer(); 255 | 256 | buffer[6] = 65.455F; 257 | buffer[7] = 24; 258 | buffer[8] = 25; 259 | } 260 | 261 | lock (server.Lock) 262 | { 263 | AsyncWorkaround(); 264 | } 265 | 266 | var client = new ModbusTcpClient(); 267 | client.Connect(endpoint); 268 | 269 | // Act 270 | var actual1 = await client.ReadWriteMultipleRegistersAsync(0, 2, 10, 12, new float[] { 1.211F }); 271 | 272 | // Assert 273 | var expected = new float[] { 0, 0, 0, 0, 0, 1.211F, 24, 25, 0, 0 }; 274 | 275 | Assert.True(expected.SequenceEqual(actual1.ToArray())); 276 | 277 | lock (server.Lock) 278 | { 279 | var actual2 = server.GetHoldingRegisterBuffer().Slice(1, 10).ToArray(); 280 | Assert.True(expected.SequenceEqual(actual2)); 281 | } 282 | } 283 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5.2.0", 3 | "suffix": "" 4 | } --------------------------------------------------------------------------------