├── .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 | [](https://github.com/Apollo3zehn/FluentModbus/actions) [](https://www.nuget.org/packages/FluentModbus)
4 |
5 | FluentModbus is a .NET Standard library (2.0 and 2.1) that provides Modbus TCP/RTU server and client implementations for easy process data exchange. Both, the server and the client, implement class 0, class 1 and class 2 (partially) functions of the [specification](http://www.modbus.org/specs.php). Namely, these are:
6 |
7 | #### Class 0:
8 | * FC03: ReadHoldingRegisters
9 | * FC16: WriteMultipleRegisters
10 |
11 | #### Class 1:
12 | * FC01: ReadCoils
13 | * FC02: ReadDiscreteInputs
14 | * FC04: ReadInputRegisters
15 | * FC05: WriteSingleCoil
16 | * FC06: WriteSingleRegister
17 |
18 | #### Class 2:
19 | * FC15: WriteMultipleCoils
20 | * FC23: ReadWriteMultipleRegisters
21 |
22 | Please see the [introduction](https://apollo3zehn.github.io/FluentModbus/) to get a more detailed description on how to use this library!
23 |
24 | Below is a screenshot of the [sample](https://apollo3zehn.github.io/FluentModbus/samples/modbus_tcp.html) console output using a Modbus TCP server and client:
25 |
26 | 
--------------------------------------------------------------------------------
/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 |
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