39 Commits
v0.3.0 ... main

Author SHA1 Message Date
83339e2ea0 Bump to v0.5.0
All checks were successful
Branch Build / build-test-deploy (push) Successful in 2m35s
Release Build / build-test-deploy (push) Successful in 2m34s
2026-01-12 19:00:20 +01:00
283eee556f Updated to .NET 10
All checks were successful
Branch Build / build-test-deploy (push) Successful in 1m49s
2026-01-12 18:55:29 +01:00
f7978c6277 Migrating to VS 2026 2026-01-12 18:31:30 +01:00
56b83f2d33 Migrated CI from gitlab to gitea 2026-01-12 18:30:35 +01:00
793a088c5b Update editorconfig 2025-08-08 17:52:06 +02:00
b216304482 Added missing snapshot tests 2025-08-07 16:42:54 +02:00
e87649ff43 Added basic information for docs 2025-08-07 16:21:10 +02:00
11295ea247 Added automatic documentation generation on Release 2025-08-07 16:20:33 +02:00
799a014b15 Moving structure as preparation for docs 2025-08-06 21:08:55 +02:00
885079ae70 Fixed wrong R/W timeout handling on tcp listener handling. v0.4.2 2025-02-07 17:05:23 +01:00
17fc216658 Bump to v0.4.1 2025-02-06 17:04:38 +01:00
885231466b Fix of #7 tried to set DualMode on IPv4 network 2025-02-06 17:02:52 +01:00
5b8a2a8af1 Changed UnitTests for further improvements on testing 2025-02-05 20:46:13 +01:00
980dab22f3 Small .NET optimizations 2025-02-03 22:29:42 +01:00
9270f49519 CLI writing to error 2025-02-03 22:28:59 +01:00
241a9d114c Async optimization 2025-02-03 22:28:31 +01:00
9283b04971 Provide license as file on NuGet.
This will provide the file also in the NuGet package and can be seen when looking into projects dependencies.
2025-02-03 22:26:06 +01:00
8b3441f6dd Bump to v0.4.0 2025-01-29 00:17:48 +01:00
63c88f5da7 Added modbus-proxy CLI tool 2025-01-28 23:55:44 +01:00
e7300bfbde Implemented UnitTests for TCP proxy 2025-01-28 21:22:11 +01:00
38dd94471d Fixed wrong ADU calculation on RTU proxy 2025-01-28 14:00:58 +01:00
56664cdac5 Added TCP wrappers to TCP proxy 2025-01-28 14:00:14 +01:00
4ef7500c3b Added some TCP wrapper classes for testability 2025-01-28 13:58:01 +01:00
05759f8e12 Fixes for SerialRtuProxy
- Adding UnitTests
- Fixing some bugs
- Updating UnitTest dependencies
2025-01-27 17:27:23 +01:00
6fc7cfda9a Updated serial implementations 2025-01-23 22:05:44 +01:00
fb67e0b77e Added VirtualModbusClient to Common 2025-01-23 22:01:45 +01:00
ce3d873cd0 Moving proxies to matching projects 2025-01-23 21:59:46 +01:00
1cf49f74ea Added README for cli tool 2025-01-23 12:05:47 +01:00
39863880d5 Added new cli tool for client connections 2025-01-21 19:27:29 +01:00
ec0ba31b86 Fixed wrong 'following bytes' information in TCP proxy 2025-01-21 19:26:13 +01:00
96b5ee21c8 Updated licensing information (still MIT) 2025-01-21 19:22:30 +01:00
6a231e02cb ReadWriteTimeout for ModbusTcpProxy changed
As suggestion of #5 on GitHub, the default value of the timeout changed.
But to the same timeout as on HttpClient and not infinite.
2024-12-13 17:58:34 +01:00
c1a70de6bb Moved InternalsVisibleTo to directory.build.props 2024-11-12 08:26:25 +01:00
6bf011d53f Updating dependencies and CI 2024-09-09 08:15:31 +02:00
0c81ab6b44 Updating to v0.3.2 2024-09-04 06:55:06 +02:00
3e8f2cd73b Addding configuration for strong named assembly build 2024-09-04 06:54:55 +02:00
e830e43c36 Clearified/fixed description of different protocol implementations 2024-07-17 18:00:16 +02:00
6a63dbb739 Bump to v0.3.1 2024-06-28 21:37:26 +02:00
1536c60336 Fixing wrong range validations 2024-06-28 21:37:00 +02:00
139 changed files with 10051 additions and 4456 deletions

View File

@@ -91,6 +91,7 @@ dotnet_naming_style.prefix_underscore.required_prefix = _
[*.cs] [*.cs]
csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:suggestion csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:suggestion
csharp_style_prefer_primary_constructors = false
# Only use "var" when it's obvious what the variable type is # Only use "var" when it's obvious what the variable type is
csharp_style_var_for_built_in_types = false:warning csharp_style_var_for_built_in_types = false:warning
@@ -157,3 +158,6 @@ csharp_space_between_square_brackets = false
[*.{xml,csproj,targets,props,json,yml}] [*.{xml,csproj,targets,props,json,yml}]
indent_size = 2 indent_size = 2
indent_style = space indent_style = space
[*.{json,yml}]
end_of_line = lf

View File

@@ -0,0 +1,60 @@
name: Branch Build
on:
push:
branches:
- '**'
env:
TZ: 'Europe/Berlin'
LANG: 'de'
CONFIGURATION: 'Debug'
GITEA_SERVER_URL: ${{ gitea.server_url }}
jobs:
build-test-deploy:
runs-on: ubuntu
steps:
- name: Checkout repository code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup dotnet
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.x
cache: false
- name: Restore dependencies
run: |
set -ex
dotnet restore -v q
echo "CI_SERVER_HOST=${GITEA_SERVER_URL#https://}" >> "$GITEA_ENV"
- name: Setup tools
run: |
set -ex
dotnet tool install dotnet-reportgenerator-globaltool --tool-path /dotnet-tools
- name: Build solution
shell: bash
run: |
set -ex
shopt -s globstar
mkdir /artifacts
dotnet build -c ${CONFIGURATION} --no-restore --nologo
mv ./**/*.nupkg /artifacts/ || true
mv ./**/*.snupkg /artifacts/ || true
- name: Test solution
run: |
set -ex
dotnet test -c ${CONFIGURATION} --no-build --nologo /p:CoverletOutputFormat=Cobertura
/dotnet-tools/reportgenerator "-reports:${{ gitea.workspace }}/**/coverage.cobertura.xml" "-targetdir:/reports" "-reportType:TextSummary"
cat /reports/Summary.txt
- name: Publish packages
run: |
set -ex
dotnet nuget push -k "${{ secrets.BAGET_APIKEY }}" -s https://nuget.am-wd.de/v3/index.json --skip-duplicate /artifacts/*.nupkg

View File

@@ -0,0 +1,78 @@
name: Release Build
on:
push:
tags:
- 'v*'
env:
TZ: 'Europe/Berlin'
LANG: 'de'
CONFIGURATION: 'Release'
GITEA_SERVER_URL: ${{ gitea.server_url }}
jobs:
build-test-deploy:
runs-on: ubuntu
steps:
- name: Checkout repository code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup dotnet
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.x
cache: false
- name: Restore dependencies
run: |
set -ex
dotnet restore -v q
echo "CI_SERVER_HOST=${GITEA_SERVER_URL#https://}" >> "$GITEA_ENV"
- name: Setup tools
run: |
set -ex
dotnet tool install dotnet-reportgenerator-globaltool --tool-path /dotnet-tools
dotnet tool install docfx --tool-path /dotnet-tools
- name: Build solution
shell: bash
run: |
set -ex
shopt -s globstar
mkdir /artifacts
dotnet build -c ${CONFIGURATION} --no-restore --nologo
mv ./**/*.nupkg /artifacts/ || true
mv ./**/*.snupkg /artifacts/ || true
- name: Test solution
run: |
set -ex
dotnet test -c ${CONFIGURATION} --no-build --nologo /p:CoverletOutputFormat=Cobertura
/dotnet-tools/reportgenerator "-reports:${{ gitea.workspace }}/**/coverage.cobertura.xml" "-targetdir:/reports" "-reportType:TextSummary"
- name: Publish packages
run: |
set -ex
dotnet nuget push -k "${{ secrets.NUGET_APIKEY }}" -s https://api.nuget.org/v3/index.json --skip-duplicate /artifacts/*.nupkg
- name: Publish documentation
env:
DOCFX_SOURCE_REPOSITORY_URL: 'https://github.com/AM-WD/AMWD.Protocols.Modbus'
run: |
set -ex
/dotnet-tools/docfx metadata docs/docfx.json
/dotnet-tools/docfx build docs/docfx.json
tar -C "${{ gitea.workspace }}/docs/_site" -czf "/artifacts/docs.tar.gz" .
curl -sSL --no-progress-meter --user "${{ secrets.DOCS_DEPLOY_USER }}:${{ secrets.DOCS_DEPLOY_PASS }}" -F docs=modbus -F dump=@/artifacts/docs.tar.gz "${{ vars.DOCS_DEPLOY_URL }}"
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: artifacts
path: |
/artifacts/*
/reports/Summary.txt

3
.gitignore vendored
View File

@@ -2,6 +2,9 @@
## Ignore Visual Studio temporary files, build results, and ## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons. ## files generated by popular Visual Studio add-ons.
docs/api
docs/_site
# User-specific files # User-specific files
*.suo *.suo
*.user *.user

View File

@@ -1,124 +0,0 @@
image: mcr.microsoft.com/dotnet/sdk:8.0
variables:
TZ: "Europe/Berlin"
LANG: "de"
stages:
- build
- test
- deploy
build-debug:
stage: build
tags:
- docker
- lnx
- 64bit
rules:
- if: $CI_COMMIT_TAG == null
script:
- dotnet restore --no-cache --force
- dotnet build -c Debug --nologo --no-restore --no-incremental
- mkdir ./artifacts
- mv ./AMWD.Protocols.Modbus.Common/bin/Debug/*.nupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Common/bin/Debug/*.snupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Debug/*.nupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Debug/*.snupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Serial/bin/Debug/*.nupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Serial/bin/Debug/*.snupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Debug/*.nupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Debug/*.snupkg ./artifacts/
artifacts:
paths:
- artifacts/*.nupkg
- artifacts/*.snupkg
expire_in: 1 days
test-debug:
stage: test
dependencies:
- build-debug
tags:
- docker
- lnx
- 64bit
rules:
- if: $CI_COMMIT_TAG == null
coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/'
script:
- dotnet restore --no-cache --force
- dotnet test -c Debug --nologo --no-restore
deploy-debug:
stage: deploy
dependencies:
- build-debug
- test-debug
tags:
- docker
- lnx
- 64bit
rules:
- if: $CI_COMMIT_TAG == null
script:
- dotnet nuget push -k $BAGET_APIKEY -s https://nuget.am-wd.de/v3/index.json --skip-duplicate artifacts/*.nupkg
build-release:
stage: build
tags:
- docker
- lnx
- amd64
rules:
- if: $CI_COMMIT_TAG != null
script:
- dotnet restore --no-cache --force
- dotnet build -c Release --nologo --no-restore --no-incremental
- mkdir ./artifacts
- mv ./AMWD.Protocols.Modbus.Common/bin/Release/*.nupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Common/bin/Release/*.snupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Release/*.nupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Release/*.snupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Serial/bin/Release/*.nupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Serial/bin/Release/*.snupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Release/*.nupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Release/*.snupkg ./artifacts/
artifacts:
paths:
- artifacts/*.nupkg
- artifacts/*.snupkg
expire_in: 1 days
test-release:
stage: test
dependencies:
- build-release
tags:
- docker
- lnx
- amd64
rules:
- if: $CI_COMMIT_TAG != null
coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/'
script:
- dotnet restore --no-cache --force
- dotnet test -c Release --nologo --no-restore
deploy-release:
stage: deploy
dependencies:
- build-release
- test-release
tags:
- docker
- lnx
- 64bit
rules:
- if: $CI_COMMIT_TAG != null
script:
- dotnet nuget push -k $NUGET_APIKEY -s https://api.nuget.org/v3/index.json --skip-duplicate artifacts/*.nupkg

View File

@@ -1,4 +0,0 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("AMWD.Protocols.Modbus.Tests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]

View File

@@ -1,42 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
<LangVersion>12.0</LangVersion>
<PackageId>AMWD.Protocols.Modbus.Proxy</PackageId>
<AssemblyName>amwd-modbus-proxy</AssemblyName>
<RootNamespace>AMWD.Protocols.Modbus.Proxy</RootNamespace>
<Product>Modbus Proxy Clients</Product>
<Description>Plugging Modbus Servers and Clients together to create Modbus Proxies.</Description>
<PackageTags>Modbus Protocol Proxy</PackageTags>
</PropertyGroup>
<ItemGroup>
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" />
<Compile Include="../AMWD.Protocols.Modbus.Tcp/Extensions/StreamExtensions.cs" Link="Extensions/StreamExtensions.cs" />
</ItemGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="/" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="System.IO.Ports" Version="4.7.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
<PackageReference Include="System.IO.Ports" Version="6.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
<ProjectReference Include="..\AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,10 +0,0 @@
# Modbus Protocol for .NET | Proxy
With this package the server and client implementations will be combined as proxy.
You can use any `ModbusBasClient` implementation as target client and plug it into the implemented `ModbusTcpProxy` or `ModbusRtuProxy`, which implement the server side.
---
Published under MIT License (see [**tl;dr**Legal](https://www.tldrlegal.com/license/mit-license))

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
namespace System.IO
{
internal static class StreamExtensions
{
public static async Task<byte[]> ReadExpectedBytesAsync(this Stream stream, int expectedBytes, CancellationToken cancellationToken = default)
{
byte[] buffer = new byte[expectedBytes];
int offset = 0;
do
{
int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken);
if (count < 1)
throw new EndOfStreamException();
offset += count;
}
while (offset < expectedBytes && !cancellationToken.IsCancellationRequested);
cancellationToken.ThrowIfCancellationRequested();
return buffer;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>12.0</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<CollectCoverage>true</CollectCoverage>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="MSTest.TestAdapter" Version="3.2.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.2.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
<ProjectReference Include="..\AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj" />
<ProjectReference Include="..\AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,5 +0,0 @@
global using System;
global using System.Linq;
global using AMWD.Protocols.Modbus.Common;
global using AMWD.Protocols.Modbus.Common.Contracts;
global using Microsoft.VisualStudio.TestTools.UnitTesting;

View File

@@ -1,78 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34525.116
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Common", "AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj", "{2B7689D8-9E56-4DEB-B40E-F70DB4A6F250}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0C43172F-63F3-455A-A5FC-CAE7492A969B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{A5A9AEA2-3AFF-4536-9FF9-34663DA4D0AD}"
ProjectSection(SolutionItems) = preProject
CHANGELOG.md = CHANGELOG.md
LICENSE.txt = LICENSE.txt
package-icon.png = package-icon.png
README.md = README.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "config", "config", "{2ED08B2B-1F72-4E1E-9586-1DC6BEFD7BA7}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitignore = .gitignore
CodeMaid.config = CodeMaid.config
nuget.config = nuget.config
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{C8065AE3-BA87-49AC-8100-C85D6DF7E436}"
ProjectSection(SolutionItems) = preProject
.gitlab-ci.yml = .gitlab-ci.yml
Directory.Build.props = Directory.Build.props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Tests", "AMWD.Protocols.Modbus.Tests\AMWD.Protocols.Modbus.Tests.csproj", "{146070C4-E922-4F5A-AD6F-9A899186E26E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Tcp", "AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj", "{8C888A84-CD09-4087-B5DA-67708ABBABA2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Serial", "AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj", "{D966826F-EE6C-4BC0-9185-C2A9A50FD586}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Proxy", "AMWD.Protocols.Modbus.Proxy\AMWD.Protocols.Modbus.Proxy.csproj", "{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2B7689D8-9E56-4DEB-B40E-F70DB4A6F250}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2B7689D8-9E56-4DEB-B40E-F70DB4A6F250}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B7689D8-9E56-4DEB-B40E-F70DB4A6F250}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2B7689D8-9E56-4DEB-B40E-F70DB4A6F250}.Release|Any CPU.Build.0 = Release|Any CPU
{146070C4-E922-4F5A-AD6F-9A899186E26E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{146070C4-E922-4F5A-AD6F-9A899186E26E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{146070C4-E922-4F5A-AD6F-9A899186E26E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{146070C4-E922-4F5A-AD6F-9A899186E26E}.Release|Any CPU.Build.0 = Release|Any CPU
{8C888A84-CD09-4087-B5DA-67708ABBABA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8C888A84-CD09-4087-B5DA-67708ABBABA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8C888A84-CD09-4087-B5DA-67708ABBABA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C888A84-CD09-4087-B5DA-67708ABBABA2}.Release|Any CPU.Build.0 = Release|Any CPU
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Release|Any CPU.Build.0 = Release|Any CPU
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{A5A9AEA2-3AFF-4536-9FF9-34663DA4D0AD} = {0C43172F-63F3-455A-A5FC-CAE7492A969B}
{2ED08B2B-1F72-4E1E-9586-1DC6BEFD7BA7} = {0C43172F-63F3-455A-A5FC-CAE7492A969B}
{C8065AE3-BA87-49AC-8100-C85D6DF7E436} = {0C43172F-63F3-455A-A5FC-CAE7492A969B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E4FD8EF0-3594-4994-BE80-5FADA5EE17B4}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,38 @@
<Solution>
<Folder Name="/Solution Items/" />
<Folder Name="/Solution Items/build/">
<File Path="Directory.Build.props" />
</Folder>
<Folder Name="/Solution Items/build/workflows/">
<File Path=".gitea/workflows/branch-build.yml" />
<File Path=".gitea/workflows/release-build.yml" />
</Folder>
<Folder Name="/Solution Items/config/">
<File Path=".editorconfig" />
<File Path=".gitignore" />
<File Path="CodeMaid.config" />
<File Path="nuget.config" />
</Folder>
<Folder Name="/Solution Items/docs/">
<File Path="CHANGELOG.md" />
<File Path="LICENSE.txt" />
<File Path="package-icon.png" />
<File Path="README.md" />
</Folder>
<Folder Name="/src/">
<Project Path="src/AMWD.Protocols.Modbus.Common/AMWD.Protocols.Modbus.Common.csproj" />
<Project Path="src/AMWD.Protocols.Modbus.Serial/AMWD.Protocols.Modbus.Serial.csproj" />
<Project Path="src/AMWD.Protocols.Modbus.Tcp/AMWD.Protocols.Modbus.Tcp.csproj" />
<File Path="src/Directory.Build.props" />
</Folder>
<Folder Name="/test/">
<File Path="test/MSTestSettings.cs" />
<Project Path="test/AMWD.Protocols.Modbus.Tests/AMWD.Protocols.Modbus.Tests.csproj" />
<File Path="test/Directory.Build.props" />
</Folder>
<Folder Name="/tool/">
<Project Path="tool/CliClient/CliClient.csproj" />
<Project Path="tool/CliProxy/CliProxy.csproj" />
<File Path="tool/Directory.Build.props" />
</Folder>
</Solution>

BIN
AMWD.Protocols.Modbus.snk Normal file

Binary file not shown.

View File

@@ -2,33 +2,105 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
_no changes_ _no changes yet_
## [v0.5.0] (2026-01-12)
### Added
- New automatic documentation generation using docfx.
- Additional articles for the documentation.
### Changed
- Reorganized folder structure to allow documentation generation.
- Migrated main repository from GitLab to Gitea.
- Migrated to VS 2026.
- Updated UnitTests and Tools to .NET 10.
## [v0.4.2] (2025-02-07)
### Fixed
- Fixing issue with R/W timeouts while processing client requests on the `ModbusTcpProxy`.
## [v0.4.1] (2025-02-06)
### Changed
- Async methods do not return on captured context anymore (`Task.ConfigureAwait(false)`).
### Fixed
- Set `Socket.DualMode` on IPv4 network address is not allowed (`ModbusTcpProxy`).
## [v0.4.0] (2025-01-29)
### Added
- Small CLI client for Modbus communication.
- Small CLI proxy to forward messages.
- `VirtualModbusClient` added to `AMWD.Protocols.Modbus.Common`.
### Changed
- The `ModbusTcpProxy.ReadWriteTimeout` has a default value of 100 seconds (same default as a `HttpClient` has).
- The `ModbusRtuProxy` moved from `AMWD.Protocols.Modbus.Proxy` to `AMWD.Protocols.Modbus.Serial`.
- The `ModbusTcpProxy` moved from `AMWD.Protocols.Modbus.Proxy` to `AMWD.Protocols.Modbus.Tcp`.
- Server implementations are proxies with a virtual Modbus client.
### Removed
- Discontinue the `AMWD.Protocols.Modbus.Proxy` package (introduced in [v0.3.0]).
### Fixed
- Wrong _following bytes_ calculation in `ModbusTcpProxy`.
- Wrong processing of `WriteMultipleHoldingRegisters` for proxies.
## [v0.3.2] (2024-09-04)
### Added
- Build configuration for strong named assemblies.
## [v0.3.1] (2024-06-28)
### Fixed
- Issues with range validation on several lines of code in server implementations.
## [v0.3.0] (2024-05-31) ## [v0.3.0] (2024-05-31)
### Added ### Added
- New `AMWD.Protocols.Modbus.Proxy` package, that contains the server implementations as proxies - New `AMWD.Protocols.Modbus.Proxy` package, that contains the server implementations as proxies.
### Changed ### Changed
- Renamed `ModbusSerialServer` to `ModbusRtuServer` to clearify the protocol that is used - Renamed `ModbusSerialServer` to `ModbusRtuServer` to clearify the protocol that is used.
- Made `Protocol` property of `ModbusClientBase` non-abstract - Made `Protocol` property of `ModbusClientBase` non-abstract.
### Fixed ### Fixed
- Issue with missing client on TCP connection when using default constructor (seems that `AddressFamily.Unknown` caused the problem) - Issue with missing client on TCP connection when using default constructor (seems that `AddressFamily.Unknown` caused the problem).
## [v0.2.0] (2024-04-02) ## [v0.2.0] (2024-04-02)
First "final" re-implementation First "final" re-implementation.
## v0.1.0 (2022-08-28) ## v0.1.0 (2022-08-28)
@@ -38,6 +110,13 @@ So this tag is only here for documentation purposes of the NuGet Gallery.
[Unreleased]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.0...HEAD [Unreleased]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.5.0...HEAD
[v0.5.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.4.2...v0.5.0
[v0.4.2]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.4.1...v0.4.2
[v0.4.1]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.4.0...v0.4.1
[v0.4.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.2...v0.4.0
[v0.3.2]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.1...v0.3.2
[v0.3.1]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.0...v0.3.1
[v0.3.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.2.0...v0.3.0 [v0.3.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.2.0...v0.3.0
[v0.2.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/tree/v0.2.0 [v0.2.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/tree/v0.2.0

View File

@@ -1,50 +1,20 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<LangVersion>14.0</LangVersion>
<NrtRevisionFormat>{semvertag:main}{!:-dev}</NrtRevisionFormat> <NrtRevisionFormat>{semvertag:main}{!:-dev}</NrtRevisionFormat>
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/AM-WD/AMWD.Protocols.Modbus.git</RepositoryUrl>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EmbedUntrackedSources>false</EmbedUntrackedSources>
<PackageIcon>package-icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageProjectUrl>https://wiki.am-wd.de/libs/modbus</PackageProjectUrl>
<Title>Modbus Protocol for .NET</Title> <Title>Modbus Protocol for .NET</Title>
<Company>AM.WD</Company> <Company>AM.WD</Company>
<Authors>Andreas Müller</Authors> <Authors>Andreas Müller</Authors>
<Copyright>© {copyright:2018-} AM.WD</Copyright> <Copyright>© {copyright:2018-} AM.WD</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<SignAssembly>true</SignAssembly>
<PublicKey>0024000004800000940000000602000000240000525341310004000001000100adcc4f9f5bb3ac73cb30661f6f35772b8f90a74412925764a960af06ef125bdcec05ed1d139503d5203fb72aa3fa74bab58e82ac2a6cd4b650f8cbf7086a71bc2dfc67e95b8d26d776d60856acf3121f831529b1a4dee91b34ac84f95f71a1165b7783edb591929ba2a684100c92bbed8859c7266fb507f6f55bb6f7fcac80b4</PublicKey>
<MoqPublicKey>0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7</MoqPublicKey>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(GITLAB_CI)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup Condition="'$(GITLAB_CI)' == 'true'">
<SourceLinkGitLabHost Include="$(CI_SERVER_HOST)" Version="$(CI_SERVER_VERSION)" />
<PackageReference Include="Microsoft.SourceLink.GitLab" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="../package-icon.png" Pack="true" PackagePath="/" /> <PackageReference Include="AMWD.NetRevisionTask" Version="1.4.0">
</ItemGroup>
<ItemGroup>
<PackageReference Include="Unclassified.NetRevisionTask" Version="0.4.3">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@@ -1,4 +1,4 @@
The MIT License MIT License
Copyright (c) Andreas Müller Copyright (c) Andreas Müller
@@ -9,8 +9,9 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions: furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in The above copyright notice and this permission notice (including the next
all copies or substantial portions of the Software. paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

View File

@@ -2,9 +2,11 @@
Here you can find a basic implementation of the Modbus protocol. Here you can find a basic implementation of the Modbus protocol.
![NuGet Version](https://shields.io/nuget/v/AMWD.Protocols.Modbus.Common?style=flat&logo=nuget)
## Overview ## Overview
The project is divided into four parts. The project is divided into multiple parts.
To be mentioned at the beginning: To be mentioned at the beginning:
Only the clients are build very modular to fit any requirement reached on the first implementation back in 2018 ([see here]). Only the clients are build very modular to fit any requirement reached on the first implementation back in 2018 ([see here]).
@@ -20,11 +22,6 @@ For example the default protocol versions: `TCP`, `RTU` and `ASCII`.
With this package you'll have anything you need to create your own client implementations. With this package you'll have anything you need to create your own client implementations.
### [Proxy]
The package contains a TCP and a RTU server implementation as proxy which contains a client of your choice to connect to.
### [Serial] ### [Serial]
This package contains some wrappers and implementations for the serial protocol. This package contains some wrappers and implementations for the serial protocol.
@@ -39,16 +36,13 @@ It uses a specific TCP connection implementation and plugs all things from the C
--- ---
Published under [MIT License] (see [**tl;dr**Legal]) Published under [MIT License] (see [choose a license])
[![Buy me a Coffee](https://shields.am-wd.de/badge/PayPal-Buy_me_a_Coffee-yellow?style=flat&logo=paypal)](https://link.am-wd.de/donate)
[![built with Codeium](https://codeium.com/badges/main)](https://link.am-wd.de/codeium)
[see here]: https://github.com/andreasAMmueller/Modbus [see here]: https://github.com/andreasAMmueller/Modbus
[Common]: AMWD.Protocols.Modbus.Common/README.md [Common]: src/AMWD.Protocols.Modbus.Common/README.md
[Proxy]: AMWD.Protocols.Modbus.Proxy/README.md [Serial]: src/AMWD.Protocols.Modbus.Serial/README.md
[Serial]: AMWD.Protocols.Modbus.Serial/README.md [TCP]: src/AMWD.Protocols.Modbus.Tcp/README.md
[TCP]: AMWD.Protocols.Modbus.Tcp/README.md
[MIT License]: LICENSE.txt [MIT License]: LICENSE.txt
[**tl;dr**Legal]: https://www.tldrlegal.com/license/mit-license [choose a license]: https://choosealicense.com/licenses/mit/

View File

@@ -0,0 +1,106 @@
# Getting Started
To begin, you need at least the [Common] package.
In this package you'll find everything you need to implement you own client as the package contains the protocol implementations (`TCP`, `RTU` and `ASCII`).
The [`ModbusClientBase`](~/api/AMWD.Protocols.Modbus.Common.Contracts.ModbusClientBase.yml) is the place, where most of the magic happens.
In this base client you have all known (and implemented) methods to request a device.
The Protocol implementations are the other magic place to be, as there the request will be converted into bits and bytes, before they get transfered.
## Using a TCP client
To use a TCP Modbus client, you need the [Common] package and the [TCP] package installed.
```cs
using AMWD.Protocols.Modbus.Common;
using AMWD.Protocols.Modbus.Common.Contracts;
using AMWD.Protocols.Modbus.Common.Protocols;
using AMWD.Protocols.Modbus.Tcp;
namespace ConsoleApp;
internal class Program
{
string hostname = "modbus-device.internal";
int port = 502;
byte unitId = 5;
ushort startAddress = 19000;
ushort count = 2;
using var client = new ModbusTcpClient(hostname, port);
await client.ConnectAsync(CancellationToken.None);
var holdingRegisters = await client.ReadHoldingRegistersAsync(unitId, startAddress, count);
float voltage = holdingRegisters.GetSingle();
Console.WriteLine($"The voltage of the device #{unitId} between L1 and N is {voltage:N2}V.");
}
```
This will automatically create a TCP client using the TCP protocol.
If you want to change the protocol sent over TCP, you can specify it:
```cs
// [...] other code
using var client = new ModbusTcpClient(hostname, port)
{
Protocol = new RtuProtocol()
};
// [...] other code
```
## Using a Serial client
To use a Serial Modbus client, you need the [Common] package and the [Serial] package installed.
```cs
using AMWD.Protocols.Modbus.Common;
using AMWD.Protocols.Modbus.Common.Contracts;
using AMWD.Protocols.Modbus.Common.Protocols;
using AMWD.Protocols.Modbus.Serial;
namespace ConsoleApp;
internal class Program
{
string serialPort = "/dev/ttyUSB0";
byte unitId = 5;
ushort startAddress = 19000;
ushort count = 2;
using var client = new ModbusSerialClient(serialPort);
await client.ConnectAsync(CancellationToken.None);
var holdingRegisters = await client.ReadHoldingRegistersAsync(unitId, startAddress, count);
float voltage = holdingRegisters.GetSingle();
Console.WriteLine($"The voltage of the device #{unitId} between L1 and N is {voltage:N2}V.");
}
```
This will automatically create a Serial client using the RTU protocol.
If you want to change the protocol sent over serial line, you can specify it:
```cs
// [...] other code
using var client = new ModbusSerialClient(serialPort)
{
Protocol = new AsciiProtocol()
};
// [...] other code
```
[Common]: https://www.nuget.org/packages/AMWD.Protocols.Modbus.Common
[Serial]: https://www.nuget.org/packages/AMWD.Protocols.Modbus.Serial
[TCP]: https://www.nuget.org/packages/AMWD.Protocols.Modbus.Tcp

View File

@@ -0,0 +1,20 @@
# Introduction
During my training, I came into contact with the Modbus protocol.
The implementation I developed at that time was very cumbersome and rigid.
There were huge inheritance hierarchies and the design was very confusing.
In 2018, I wanted to do better and completely redesigned the library.
After changing companies, this library could be integrated and tested under real-world conditions.
This quickly led to new challenges and some specific requirements were implemented.
This was the first time that both TCP and the RTU protocol were fully implemented.
However, the structure of the library also revealed problems and was too rigid for the requirements.
Therefore, in 2024, there was a new development from scratch, which now exists and has already been tested by some eager people THANK YOU SO MUCH!
The focus is, of course, on the development of the protocol and the clients. However, a server implementation (TCP/RTU) is also available.
For detailed changes of the current development, see the [CHANGELOG].
[CHANGELOG]: https://github.com/AM-WD/AMWD.Protocols.Modbus/blob/main/CHANGELOG.md

8
docs/articles/toc.yml Normal file
View File

@@ -0,0 +1,8 @@
- name: Introduction
href: introduction.md
- name: Getting Started
href: getting-started.md
- name: GitHub
href: https://github.com/AM-WD/AMWD.Protocols.Modbus
- name: NuGet
href: https://www.nuget.org/packages?q=AMWD.Protocols.Modbus

63
docs/docfx.json Normal file
View File

@@ -0,0 +1,63 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/docfx/main/schemas/docfx.schema.json",
"metadata": [
{
"src": [
{
"src": "../",
"files": [
"src/AMWD.Protocols.Modbus.Common/bin/Release/netstandard2.0/amwd-modbus-common.dll",
"src/AMWD.Protocols.Modbus.Serial/bin/Release/netstandard2.0/amwd-modbus-serial.dll",
"src/AMWD.Protocols.Modbus.Tcp/bin/Release/netstandard2.0/amwd-modbus-tcp.dll"
]
}
],
"dest": "api",
"outputFormat": "apiPage"
}
],
"build": {
"content": [
{
"files": [ "**/*.{md,yml}" ],
"exclude": [ "_site/**", "obj/**" ]
}
],
"resource": [
{
"files": [ "images/**" ],
"exclude": [ "_site/**", "obj/**" ]
}
],
"output": "_site",
"template": [ "default", "modern", "templates/amwd" ],
"postProcessors": ["ExtractSearchIndex"],
"globalMetadata": {
"_appName": "Protocol for .NET",
"_appTitle": "Modbus Protocol for .NET",
"_appFooter": "<span>&copy; AM.WD &mdash; Docs generated using <a href=\"https://dotnet.github.io/docfx\" target=\"_blank\">docfx</a>.</span>",
"_appLogoPath": "images/logo.svg",
"_appFaviconPath": "images/favicon.ico",
"_disableBreadcrumb": true,
"_disableContribution": true,
"_enableSearch": true,
"_enableNewTab": true,
"pdf": false
},
"markdownEngineName": "markdig",
"markdownEngineProperties": {
"alerts": {
"TODO": "alert alert-secondary"
}
},
"sitemap": {
"baseUrl": "https://docs.am-wd.de/modbus",
"priority": 0.5,
"changefreq": "weekly"
},
"noLangKeyword": false,
"keepFileLink": false,
"cleanupCacheHistory": false,
"disableGitFeatures": true
}
}

BIN
docs/images/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

168
docs/images/logo.svg Normal file
View File

@@ -0,0 +1,168 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="103.59954"
height="35"
viewBox="0 0 172.52591 58.286041"
version="1.1"
id="svg17631"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="Logo of Modbus.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview17633"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.64052329"
inkscape:cx="352.0559"
inkscape:cy="108.50503"
inkscape:window-width="1850"
inkscape:window-height="1136"
inkscape:window-x="70"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs17628">
<clipPath
id="clip1">
<path
d="M 54,12.761719 H 76 V 34 H 54 Z m 0,0"
id="path619" />
</clipPath>
<clipPath
id="clip2">
<path
d="M 71,73 H 93 V 94.441406 H 71 Z m 0,0"
id="path622" />
</clipPath>
<clipPath
id="clip3">
<path
d="M 32.160156,35 H 54 V 57 H 32.160156 Z m 0,0"
id="path625" />
</clipPath>
<clipPath
id="clip4">
<path
d="m 247,46 h 26.92578 V 67 H 247 Z m 0,0"
id="path628" />
</clipPath>
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-11.970621,-119.59261)">
<path
style="fill:#f5911c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.713609"
d="m 61.396335,149.27146 c 0,11.04421 -8.9452,20.00055 -19.983825,20.00055 -11.035839,0 -19.983816,-8.95634 -19.983816,-20.00055 0,-11.04698 8.947977,-20.00054 19.983816,-20.00054 11.038625,0 19.983825,8.95356 19.983825,20.00054"
id="path944" />
<path
style="fill:#fdbf0c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.713609"
d="m 64.409645,132.37902 c 0.14496,4.13949 -3.09137,7.61274 -7.22807,7.7577 -4.13669,0.14503 -7.60717,-3.09416 -7.75212,-7.23365 -0.14217,-4.13947 3.09416,-7.61275 7.22807,-7.7577 4.1367,-0.14504 7.60718,3.09415 7.75212,7.23365"
id="path946" />
<path
style="fill:#fdbf0c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.713609"
d="m 33.038765,164.56666 c 0.144956,4.13949 -3.091374,7.61274 -7.22807,7.7577 -4.136699,0.14503 -7.607177,-3.09416 -7.752129,-7.23365 -0.144956,-4.13949 3.091375,-7.61275 7.228071,-7.7577 4.136698,-0.14504 7.607177,3.09415 7.752128,7.23365"
id="path948" />
<g
clip-path="url(#clip1)"
clip-rule="nonzero"
id="g952"
transform="matrix(0.7136082,0,0,0.7136082,-10.979154,110.48574)">
<path
style="fill:#fdbf0c;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 69.574219,13.992188 c 5.121093,2.726562 7.066406,9.089843 4.339843,14.214843 -2.722656,5.125 -9.078124,7.070313 -14.199218,4.34375 -5.121094,-2.722656 -7.066406,-9.085937 -4.34375,-14.210937 2.722656,-5.125 9.082031,-7.074219 14.203125,-4.347656"
id="path950" />
</g>
<g
clip-path="url(#clip2)"
clip-rule="nonzero"
id="g956"
transform="matrix(0.7136082,0,0,0.7136082,-10.979154,110.48574)">
<path
style="fill:#fdbf0c;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 86.625,74.648438 c 5.121094,2.722656 7.0625,9.085937 4.339844,14.214843 -2.722656,5.121094 -9.078125,7.070313 -14.199219,4.34375 -5.121094,-2.722656 -7.066406,-9.089843 -4.34375,-14.214843 2.722656,-5.125 9.082031,-7.070313 14.203125,-4.34375"
id="path954" />
</g>
<g
clip-path="url(#clip3)"
clip-rule="nonzero"
id="g960"
transform="matrix(0.7136082,0,0,0.7136082,-10.979154,110.48574)">
<path
style="fill:#fdbf0c;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 37.097656,36.910156 c 4.917969,-3.074218 11.398438,-1.578125 14.472656,3.34375 3.074219,4.925782 1.578126,11.40625 -3.34375,14.484375 -4.917968,3.074219 -11.394531,1.578125 -14.46875,-3.34375 -3.074218,-4.921875 -1.578124,-11.40625 3.339844,-14.484375"
id="path958" />
</g>
<path
style="fill:#fdbf0c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.713609"
d="m 59.032505,147.92229 c 3.5095,-2.19377 8.13401,-1.12616 10.3278,2.38613 2.191,3.51229 1.12338,8.13959 -2.38613,10.33618 -3.5095,2.19377 -8.13123,1.12617 -10.32501,-2.38613 -2.19379,-3.51229 -1.12617,-8.13959 2.38334,-10.33618"
id="path962" />
<path
style="fill:#008cc7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.713609"
d="m 86.963565,148.26516 c -0.76378,0.61047 -1.06761,1.52758 -1.22094,2.44188 -0.15333,0.91711 -0.30662,2.14084 0,2.60077 0.30385,0.45436 1.83143,0.61046 1.83143,0.61046 h 3.5095 c 0,0 1.68087,0 2.44467,-0.45993 0.76377,-0.45715 0.91429,-1.37703 1.21814,-2.5952 0.30941,-1.22373 0.15333,-1.98751 0,-2.44467 -0.1505,-0.45993 -1.67809,-0.61047 -1.67809,-0.61047 h -3.96667 c 0,0 -1.37424,-0.15333 -2.13804,0.45716 z m 2.13804,-4.20359 h 5.03708 c 0,0 3.66282,-0.22858 5.03706,1.37704 1.374245,1.60562 0.68574,3.89418 0.45716,5.4998 -0.22858,1.60283 -0.68573,3.89418 -2.28856,5.27122 -1.60283,1.37425 -4.50186,1.45231 -5.64755,1.6781 -1.14567,0.23137 -6.94653,-0.0753 -6.94653,-0.0753 0,0 -2.67324,-0.0752 -4.35133,-1.8342 -0.65228,-0.68295 0,-3.43702 0.45716,-5.4998 0.45994,-2.05999 1.37425,-4.35132 2.7485,-5.27122 1.37425,-0.91709 5.49701,-1.14567 5.49701,-1.14567"
id="path964" />
<path
style="fill:#008cc7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.713609"
d="m 107.37108,148.28468 c -0.76377,0.61047 -1.07039,1.52756 -1.22093,2.44467 -0.15333,0.91709 -0.30383,2.13803 0,2.59518 0.30385,0.45996 1.8314,0.61327 1.8314,0.61327 h 3.50951 c 0,0 1.68088,0 2.44466,-0.45996 0.7638,-0.45715 0.91432,-1.37424 1.22094,-2.59518 0.30385,-1.22373 0.1505,-1.98751 0,-2.44467 -0.1505,-0.45995 -1.68089,-0.61326 -1.68089,-0.61326 h -3.96664 c 0,0 -1.37704,-0.1505 -2.13805,0.45995 z m 11.29229,-9.59748 h 4.73323 l -3.97222,19.09738 h -4.73323 l 0.15616,-1.9875 c 0,0 -0.092,0.61882 -1.8342,1.22373 -2.15755,0.74984 -3.8245,0.85298 -3.8245,0.85298 l -4.72485,-0.24252 c 0,0 -1.67531,0 -2.44188,-1.22093 -0.76101,-1.22373 -0.30385,-4.12556 0,-5.34929 0.30662,-1.22373 1.12338,-4.97295 2.7485,-6.11027 1.15404,-0.80838 2.44187,-1.0704 3.20567,-1.0704 0.76378,0 5.03706,0 5.03706,0 0,0 2.13805,0 2.74852,0.76378 0.61324,0.76378 1.0676,1.52756 1.0676,1.52756 l 1.8342,-7.48452"
id="path966" />
<path
style="fill:#008cc7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.713609"
d="m 130.42119,147.97804 c -0.76378,0.61047 -1.06763,1.52758 -1.22094,2.44467 -0.15333,0.91711 -0.30384,2.13805 0,2.59798 0.30383,0.45715 1.83141,0.61046 1.83141,0.61046 h 3.51229 c 0,0 1.6781,0 2.44188,-0.45993 0.76099,-0.45715 0.91709,-1.37424 1.22094,-2.59518 0.30383,-1.22373 0.1505,-1.98753 0,-2.44467 -0.15333,-0.45716 -1.6781,-0.61326 -1.6781,-0.61326 h -3.96945 c 0,0 -1.37425,-0.1505 -2.13803,0.45993 z m -2.44746,9.34661 -5.03706,0.15333 4.12277,-19.09459 h 4.73044 l -1.6781,7.63783 c 0,0 1.22094,-1.22652 2.59519,-1.68089 1.37147,-0.45993 4.57992,-0.61047 4.57992,-0.61047 0,0 3.51229,-0.15333 4.88375,0.7638 1.37425,0.9143 2.13802,1.83418 1.06763,5.95974 -1.06763,4.12276 -1.7422,5.2322 -2.89904,6.10748 -0.87807,0.669 -3.66281,1.37703 -5.80365,1.37703 -2.13246,0 -4.27328,0.1505 -6.10469,-2.59797 l -0.45716,1.98473"
id="path968" />
<path
style="fill:#008cc7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.713609"
d="m 146.64742,144.14798 h 5.03706 l -1.37425,6.45593 c -0.17558,1.02859 -0.87807,2.89066 0.68574,3.30601 1.98471,0.52964 5.94023,0.6272 7.19183,-1.87601 0.64393,-1.28783 2.04884,-7.88593 2.04884,-7.88593 h 4.73044 l -2.74851,13.6366 h -4.88654 l 0.76377,-2.59797 c 0,0 -0.91429,0.76378 -2.13523,1.37424 -1.22373,0.61327 -2.59521,1.53037 -5.80086,1.37704 -3.20845,-0.15333 -5.64755,-1.37704 -5.3437,-3.66837 0.30383,-2.29136 1.83141,-10.12154 1.83141,-10.12154"
id="path970" />
<g
clip-path="url(#clip4)"
clip-rule="nonzero"
id="g974"
transform="matrix(0.7136082,0,0,0.7136082,-10.979154,110.48574)">
<path
style="fill:#008cc7;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 267.16016,52.363281 h 6.63281 c 0,0 0.63281,-2.773437 -0.85156,-3.851562 -2.12891,-1.542969 -3.64063,-1.925781 -6.41797,-1.925781 -2.78516,0 -7.70313,0 -7.70313,0 0,0 -2.46484,0.08984 -5.13281,1.070312 -1.90625,0.699219 -3.42578,2.140625 -3.85156,5.136719 -0.42969,2.996093 0,4.710937 2.99218,5.351562 2.9961,0.644531 7.0625,0 7.0625,0 0,0 5.5586,-0.210937 6.41797,0.859375 0.85157,1.070313 0.42578,1.710938 -0.85937,2.351563 -1.28516,0.640625 -3.63281,0.855469 -5.5586,0.855469 -1.92578,0 -4.70703,0 -5.13671,-0.855469 -0.42579,-0.855469 -0.42579,-1.5 -0.42579,-1.5 h -6.41796 c 0,0 -1.07032,3 -0.21485,3.855469 0.85938,0.855468 0.85938,2.570312 8.98438,2.785156 8.1289,0.210937 8.14453,-0.234375 9.84375,-0.429688 1.69922,-0.199218 4.48437,-1.285156 5.5625,-3.640625 1.0664,-2.351562 1.37109,-5.894531 0.85547,-6.636719 -0.95704,-1.367187 -3.21094,-1.925781 -5.13672,-1.925781 -1.92578,0 -9.62891,-0.214843 -9.62891,-0.214843 0,0 -2.13672,0.214843 -1.92187,-1.285157 0.21093,-1.5 2.99609,-1.5 4.92187,-1.5 1.92188,0 5.13281,0.214844 5.77344,0.859375 0.64062,0.640625 0.21094,0.640625 0.21094,0.640625"
id="path972" />
</g>
<path
style="fill:#2bb34a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.713609"
d="m 40.481474,142.70405 c 0.730335,1.32964 1.87601,0.10033 1.87601,0.10033 l -1.485754,4.06981 -3.336677,-2.75966 c 0,0 1.608406,0.47945 1.569382,-1.03697 -0.04181,-1.51641 -1.970785,-8.63018 -2.701119,-9.95706 -0.730333,-1.32964 -1.876009,-0.10316 -1.876009,-0.10316 l 1.485753,-4.06701 3.336677,2.75687 c 0,0 -1.608406,-0.47666 -1.566592,1.03975 0.03902,1.51364 1.967998,8.62741 2.698329,9.95708"
id="path976" />
<path
style="fill:#2bb34a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.713609"
d="m 46.577808,165.27748 c 0.730337,1.32964 1.876007,0.10316 1.876007,0.10316 l -1.48575,4.06979 -3.336678,-2.75966 c 0,0 1.608406,0.47667 1.56938,-1.03974 -0.04181,-1.51364 -1.970786,-8.62741 -2.701119,-9.95708 -0.727546,-1.32685 -1.87601,-0.10033 -1.87601,-0.10033 l 1.485756,-4.06699 3.336675,2.75687 c 0,0 -1.608406,-0.47947 -1.56938,1.03696 0.04181,1.51641 1.970785,8.63018 2.701119,9.95706"
id="path978" />
<path
style="fill:#2bb34a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.713609"
d="m 46.608472,145.07624 c -0.786085,1.29618 0.850193,1.67807 0.850193,1.67807 l -4.264921,0.74428 0.719184,-4.27049 c 0,0 0.390254,1.6335 1.683669,0.84182 1.290628,-0.79444 6.481008,-6.02106 7.264308,-7.32004 0.78608,-1.29621 -0.8502,-1.6781 -0.8502,-1.6781 l 4.26493,-0.74428 -0.71919,4.27049 c 0,0 -0.39025,-1.63349 -1.68088,-0.83902 -1.29341,0.79165 -6.4838,6.02106 -7.267093,7.31727"
id="path980" />
<path
style="fill:#2bb34a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.713609"
d="m 30.123007,161.6481 c -0.786085,1.29621 0.847408,1.6781 0.847408,1.6781 l -4.262136,0.74707 0.719184,-4.2733 c 0,0 0.390254,1.63349 1.680882,0.84184 1.290628,-0.79445 6.48101,-6.02107 7.267095,-7.32007 0.786084,-1.2962 -0.850196,-1.6753 -0.850196,-1.6753 l 4.262136,-0.74705 -0.719184,4.27049 c 0,0 -0.387466,-1.63349 -1.680882,-0.84184 -1.290626,0.79446 -6.481012,6.02385 -7.264307,7.32006"
id="path982" />
<path
style="fill:#2bb34a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.713609"
d="m 47.617555,151.57117 c -1.516416,-0.0307 -1.02581,1.57497 -1.02581,1.57497 l -2.779169,-3.31996 4.055859,-1.51083 c 0,0 -1.218152,1.15402 0.11429,1.87601 1.33244,0.72197 8.4518,2.60633 9.96543,2.63979 1.51642,0.0307 1.0286,-1.57775 1.0286,-1.57775 l 2.77638,3.32274 -4.05586,1.51085 c 0,0 1.21815,-1.15405 -0.11429,-1.87601 -1.33244,-0.72199 -8.4518,-2.60635 -9.96543,-2.63981"
id="path984" />
<path
style="fill:#2bb34a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.713609"
d="m 25.03576,145.56963 c -1.516418,-0.0334 -1.028599,1.57495 -1.028599,1.57495 l -2.77917,-3.32274 4.055858,-1.51085 c 0,0 -1.21815,1.15405 0.11429,1.87601 1.335227,0.72197 8.451798,2.60913 9.968214,2.63979 1.51363,0.0334 1.025811,-1.57495 1.025811,-1.57495 l 2.776384,3.32273 -4.05586,1.51085 c 0,0 1.22094,-1.15404 -0.111498,-1.8788 -1.332442,-0.71917 -8.451798,-2.60633 -9.965428,-2.63699"
id="path986" />
<path
style="fill:#008cc7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.713609"
d="m 44.501097,157.93789 h 5.954168 l 2.97708,-13.51952 5.03707,13.29094 h 5.03708 l 10.76544,-13.29094 -2.7485,13.51952 h 5.49422 l 4.35134,-19.01932 h -9.15983 l -9.61698,11.68534 -4.34856,-11.91671 h -9.61977 l -4.122758,19.25069"
id="path988" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

26
docs/index.md Normal file
View File

@@ -0,0 +1,26 @@
---
_layout: landing
---
# Modbus Protocol for .NET
This library implements the basic Modbus protocol specified at [modbus.org](https://modbus.org/tech.php).
The aim was to include all necessary steps to have a fully working client, which is capable of the common protocol versions.
## NuGet packages
Here is an overview of the latest packages.
| Package Url | Version | Short description |
|-------------|---------|-------------------|
| [AMWD.Protocols.Modbus.Common] | ![NuGet Version: Common](https://img.shields.io/nuget/v/AMWD.Protocols.Modbus.Common?style=flat-square&logo=nuget) | Common data for Modbus protocol. |
| [AMWD.Protocols.Modbus.Serial] | ![NuGet Version: Serial](https://img.shields.io/nuget/v/AMWD.Protocols.Modbus.Serial?style=flat-square&logo=nuget) | Implementation of the Modbus protocol communicating via serial line using RTU or ASCII encoding. |
| [AMWD.Protocols.Modbus.Tcp] | ![NuGet Version: TCP](https://img.shields.io/nuget/v/AMWD.Protocols.Modbus.Tcp?style=flat-square&logo=nuget) | Implementation of the Modbus protocol communicating via TCP. |
[AMWD.Protocols.Modbus.Common]: https://www.nuget.org/packages/AMWD.Protocols.Modbus.Common
[AMWD.Protocols.Modbus.Serial]: https://www.nuget.org/packages/AMWD.Protocols.Modbus.Serial
[AMWD.Protocols.Modbus.Tcp]: https://www.nuget.org/packages/AMWD.Protocols.Modbus.Tcp

3
docs/templates/amwd/public/main.css vendored Normal file
View File

@@ -0,0 +1,3 @@
#logo {
margin-right: 8px;
}

4
docs/toc.yml Normal file
View File

@@ -0,0 +1,4 @@
- name: API
href: api/
- name: Articles
href: articles/

View File

@@ -1,9 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
<LangVersion>12.0</LangVersion>
<PackageId>AMWD.Protocols.Modbus.Common</PackageId> <PackageId>AMWD.Protocols.Modbus.Common</PackageId>
<AssemblyName>amwd-modbus-common</AssemblyName> <AssemblyName>amwd-modbus-common</AssemblyName>
<RootNamespace>AMWD.Protocols.Modbus.Common</RootNamespace> <RootNamespace>AMWD.Protocols.Modbus.Common</RootNamespace>
@@ -13,8 +10,4 @@
<PackageTags>Modbus Protocol</PackageTags> <PackageTags>Modbus Protocol</PackageTags>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="/" />
</ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,24 @@
using System;
using System.Threading.Tasks;
using System.Threading;
namespace AMWD.Protocols.Modbus.Common.Contracts
{
/// <summary>
/// Represents a Modbus proxy.
/// </summary>
public interface IModbusProxy : IDisposable
{
/// <summary>
/// Starts the proxy.
/// </summary>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
Task StartAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Stops the proxy.
/// </summary>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
Task StopAsync(CancellationToken cancellationToken = default);
}
}

View File

@@ -1,28 +1,36 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace AMWD.Protocols.Modbus.Common.Contracts namespace AMWD.Protocols.Modbus.Common.Contracts
{ {
/// <summary> /// <summary>
/// Base implementation of a Modbus client. /// Base implementation of a Modbus client.
/// </summary> /// </summary>
public abstract class ModbusClientBase : IDisposable /// <remarks>
/// Initializes a new instance of the <see cref="ModbusClientBase"/> class with a specific <see cref="IModbusConnection"/>.
/// </remarks>
/// <param name="connection">The <see cref="IModbusConnection"/> responsible for invoking the requests.</param>
/// <param name="disposeConnection">
/// <see langword="true"/> if the connection should be disposed of by Dispose(),
/// <see langword="false"/> otherwise if you inted to reuse the connection.
/// </param>
public abstract class ModbusClientBase(IModbusConnection connection, bool disposeConnection) : IDisposable
{ {
private bool _isDisposed; private bool _isDisposed;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the connection should be disposed of by <see cref="Dispose()"/>. /// Gets or sets a value indicating whether the connection should be disposed of by <see cref="Dispose()"/>.
/// </summary> /// </summary>
protected readonly bool disposeConnection; protected readonly bool disposeConnection = disposeConnection;
/// <summary> /// <summary>
/// Gets or sets the <see cref="IModbusConnection"/> responsible for invoking the requests. /// Gets or sets the <see cref="IModbusConnection"/> responsible for invoking the requests.
/// </summary> /// </summary>
protected readonly IModbusConnection connection; protected readonly IModbusConnection connection = connection ?? throw new ArgumentNullException(nameof(connection));
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ModbusClientBase"/> class with a specific <see cref="IModbusConnection"/>. /// Initializes a new instance of the <see cref="ModbusClientBase"/> class with a specific <see cref="IModbusConnection"/>.
@@ -32,20 +40,6 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
: this(connection, true) : this(connection, true)
{ } { }
/// <summary>
/// Initializes a new instance of the <see cref="ModbusClientBase"/> class with a specific <see cref="IModbusConnection"/>.
/// </summary>
/// <param name="connection">The <see cref="IModbusConnection"/> responsible for invoking the requests.</param>
/// <param name="disposeConnection">
/// <see langword="true"/> if the connection should be disposed of by Dispose(),
/// <see langword="false"/> otherwise if you inted to reuse the connection.
/// </param>
public ModbusClientBase(IModbusConnection connection, bool disposeConnection)
{
this.connection = connection ?? throw new ArgumentNullException(nameof(connection));
this.disposeConnection = disposeConnection;
}
/// <summary> /// <summary>
/// Gets or sets the protocol type to use. /// Gets or sets the protocol type to use.
/// </summary> /// </summary>
@@ -67,7 +61,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
Assertions(); Assertions();
var request = Protocol.SerializeReadCoils(unitId, startAddress, count); var request = Protocol.SerializeReadCoils(unitId, startAddress, count);
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken); var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
Protocol.ValidateResponse(request, response); Protocol.ValidateResponse(request, response);
// The protocol processes complete bytes from the response. // The protocol processes complete bytes from the response.
@@ -92,7 +86,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
Assertions(); Assertions();
var request = Protocol.SerializeReadDiscreteInputs(unitId, startAddress, count); var request = Protocol.SerializeReadDiscreteInputs(unitId, startAddress, count);
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken); var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
Protocol.ValidateResponse(request, response); Protocol.ValidateResponse(request, response);
// The protocol processes complete bytes from the response. // The protocol processes complete bytes from the response.
@@ -117,7 +111,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
Assertions(); Assertions();
var request = Protocol.SerializeReadHoldingRegisters(unitId, startAddress, count); var request = Protocol.SerializeReadHoldingRegisters(unitId, startAddress, count);
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken); var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
Protocol.ValidateResponse(request, response); Protocol.ValidateResponse(request, response);
var holdingRegisters = Protocol.DeserializeReadHoldingRegisters(response).ToList(); var holdingRegisters = Protocol.DeserializeReadHoldingRegisters(response).ToList();
@@ -140,7 +134,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
Assertions(); Assertions();
var request = Protocol.SerializeReadInputRegisters(unitId, startAddress, count); var request = Protocol.SerializeReadInputRegisters(unitId, startAddress, count);
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken); var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
Protocol.ValidateResponse(request, response); Protocol.ValidateResponse(request, response);
var inputRegisters = Protocol.DeserializeReadInputRegisters(response).ToList(); var inputRegisters = Protocol.DeserializeReadInputRegisters(response).ToList();
@@ -184,7 +178,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
do do
{ {
var request = Protocol.SerializeReadDeviceIdentification(unitId, category, requestObjectId); var request = Protocol.SerializeReadDeviceIdentification(unitId, category, requestObjectId);
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken); var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
Protocol.ValidateResponse(request, response); Protocol.ValidateResponse(request, response);
result = Protocol.DeserializeReadDeviceIdentification(response); result = Protocol.DeserializeReadDeviceIdentification(response);
@@ -247,7 +241,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
Assertions(); Assertions();
var request = Protocol.SerializeWriteSingleCoil(unitId, coil); var request = Protocol.SerializeWriteSingleCoil(unitId, coil);
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken); var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
Protocol.ValidateResponse(request, response); Protocol.ValidateResponse(request, response);
var result = Protocol.DeserializeWriteSingleCoil(response); var result = Protocol.DeserializeWriteSingleCoil(response);
@@ -268,7 +262,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
Assertions(); Assertions();
var request = Protocol.SerializeWriteSingleHoldingRegister(unitId, register); var request = Protocol.SerializeWriteSingleHoldingRegister(unitId, register);
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken); var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
Protocol.ValidateResponse(request, response); Protocol.ValidateResponse(request, response);
var result = Protocol.DeserializeWriteSingleHoldingRegister(response); var result = Protocol.DeserializeWriteSingleHoldingRegister(response);
@@ -289,7 +283,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
Assertions(); Assertions();
var request = Protocol.SerializeWriteMultipleCoils(unitId, coils); var request = Protocol.SerializeWriteMultipleCoils(unitId, coils);
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken); var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
Protocol.ValidateResponse(request, response); Protocol.ValidateResponse(request, response);
var (firstAddress, count) = Protocol.DeserializeWriteMultipleCoils(response); var (firstAddress, count) = Protocol.DeserializeWriteMultipleCoils(response);
@@ -309,7 +303,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
Assertions(); Assertions();
var request = Protocol.SerializeWriteMultipleHoldingRegisters(unitId, registers); var request = Protocol.SerializeWriteMultipleHoldingRegisters(unitId, registers);
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken); var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
Protocol.ValidateResponse(request, response); Protocol.ValidateResponse(request, response);
var (firstAddress, count) = Protocol.DeserializeWriteMultipleHoldingRegisters(response); var (firstAddress, count) = Protocol.DeserializeWriteMultipleHoldingRegisters(response);

View File

@@ -8,19 +8,26 @@ namespace AMWD.Protocols.Modbus.Common.Events
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public class CoilWrittenEventArgs : EventArgs public class CoilWrittenEventArgs : EventArgs
{ {
internal CoilWrittenEventArgs(byte unitId, ushort address, bool value)
{
UnitId = unitId;
Address = address;
Value = value;
}
/// <summary> /// <summary>
/// Gets or sets the unit id. /// Gets or sets the unit id.
/// </summary> /// </summary>
public byte UnitId { get; set; } public byte UnitId { get; }
/// <summary> /// <summary>
/// Gets or sets the coil address. /// Gets or sets the coil address.
/// </summary> /// </summary>
public ushort Address { get; set; } public ushort Address { get; }
/// <summary> /// <summary>
/// Gets or sets the coil value. /// Gets or sets the coil value.
/// </summary> /// </summary>
public bool Value { get; set; } public bool Value { get; }
} }
} }

View File

@@ -8,29 +8,39 @@ namespace AMWD.Protocols.Modbus.Common.Events
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public class RegisterWrittenEventArgs : EventArgs public class RegisterWrittenEventArgs : EventArgs
{ {
internal RegisterWrittenEventArgs(byte unitId, ushort address, byte highByte, byte lowByte)
{
UnitId = unitId;
Address = address;
HighByte = highByte;
LowByte = lowByte;
Value = new[] { highByte, lowByte }.GetBigEndianUInt16();
}
/// <summary> /// <summary>
/// Gets or sets the unit id. /// Gets or sets the unit id.
/// </summary> /// </summary>
public byte UnitId { get; set; } public byte UnitId { get; }
/// <summary> /// <summary>
/// Gets or sets the address of the register. /// Gets or sets the address of the register.
/// </summary> /// </summary>
public ushort Address { get; set; } public ushort Address { get; }
/// <summary> /// <summary>
/// Gets or sets the value of the register. /// Gets or sets the value of the register.
/// </summary> /// </summary>
public ushort Value { get; set; } public ushort Value { get; }
/// <summary> /// <summary>
/// Gets or sets the high byte of the register. /// Gets or sets the high byte of the register.
/// </summary> /// </summary>
public byte HighByte { get; set; } public byte HighByte { get; }
/// <summary> /// <summary>
/// Gets or sets the low byte of the register. /// Gets or sets the low byte of the register.
/// </summary> /// </summary>
public byte LowByte { get; set; } public byte LowByte { get; }
} }
} }

View File

@@ -1,6 +1,8 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
#if !NET8_0_OR_GREATER
using System.Runtime.Serialization; using System.Runtime.Serialization;
#endif
namespace AMWD.Protocols.Modbus.Common namespace AMWD.Protocols.Modbus.Common
{ {

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace AMWD.Protocols.Modbus.Common namespace AMWD.Protocols.Modbus.Common
@@ -12,14 +13,14 @@ namespace AMWD.Protocols.Modbus.Common
Array.Reverse(bytes); Array.Reverse(bytes);
} }
public static ushort GetBigEndianUInt16(this byte[] bytes, int offset = 0) public static ushort GetBigEndianUInt16(this IReadOnlyList<byte> bytes, int offset = 0)
{ {
byte[] b = bytes.Skip(offset).Take(2).ToArray(); byte[] b = bytes.Skip(offset).Take(2).ToArray();
b.SwapBigEndian(); b.SwapBigEndian();
return BitConverter.ToUInt16(b, 0); return BitConverter.ToUInt16(b, 0);
} }
public static byte[] ToBigEndianBytes(this ushort value) public static IReadOnlyList<byte> ToBigEndianBytes(this ushort value)
{ {
byte[] b = BitConverter.GetBytes(value); byte[] b = BitConverter.GetBytes(value);
b.SwapBigEndian(); b.SwapBigEndian();

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
namespace AMWD.Protocols.Modbus.Common namespace AMWD.Protocols.Modbus.Common
{ {
@@ -91,5 +92,23 @@ namespace AMWD.Protocols.Modbus.Common
/// Gets or sets a value indicating whether individual access (<see cref="ModbusDeviceIdentificationCategory.Individual"/>) is allowed. /// Gets or sets a value indicating whether individual access (<see cref="ModbusDeviceIdentificationCategory.Individual"/>) is allowed.
/// </summary> /// </summary>
public bool IsIndividualAccessAllowed { get; set; } public bool IsIndividualAccessAllowed { get; set; }
/// <inheritdoc/>
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine(nameof(DeviceIdentification));
sb.AppendLine($" {nameof(VendorName)}: {VendorName}");
sb.AppendLine($" {nameof(ProductCode)}: {ProductCode}");
sb.AppendLine($" {nameof(MajorMinorRevision)}: {MajorMinorRevision}");
sb.AppendLine($" {nameof(VendorUrl)}: {VendorUrl}");
sb.AppendLine($" {nameof(ProductName)}: {ProductName}");
sb.AppendLine($" {nameof(ModelName)}: {ModelName}");
sb.AppendLine($" {nameof(UserApplicationName)}: {UserApplicationName}");
sb.AppendLine($" {nameof(IsIndividualAccessAllowed)}: {IsIndividualAccessAllowed}");
return sb.ToString();
}
} }
} }

View File

@@ -17,15 +17,11 @@ namespace AMWD.Protocols.Modbus.Common
{ {
get get
{ {
byte[] blob = [HighByte, LowByte]; return new[] { HighByte, LowByte }.GetBigEndianUInt16();
blob.SwapBigEndian();
return BitConverter.ToUInt16(blob, 0);
} }
set set
{ {
byte[] blob = BitConverter.GetBytes(value); var blob = value.ToBigEndianBytes();
blob.SwapBigEndian();
HighByte = blob[0]; HighByte = blob[0];
LowByte = blob[1]; LowByte = blob[1];
} }

View File

@@ -17,9 +17,7 @@ namespace AMWD.Protocols.Modbus.Common
{ {
get get
{ {
byte[] blob = [HighByte, LowByte]; return new[] { HighByte, LowByte }.GetBigEndianUInt16();
blob.SwapBigEndian();
return BitConverter.ToUInt16(blob, 0);
} }
} }

View File

@@ -11,7 +11,7 @@ namespace AMWD.Protocols.Modbus.Common.Models
/// Initializes a new instance of the <see cref="ModbusDevice"/> class. /// Initializes a new instance of the <see cref="ModbusDevice"/> class.
/// </remarks> /// </remarks>
/// <param name="id">The <see cref="ModbusDevice"/> ID.</param> /// <param name="id">The <see cref="ModbusDevice"/> ID.</param>
public class ModbusDevice(byte id) : IDisposable internal class ModbusDevice(byte id) : IDisposable
{ {
private readonly ReaderWriterLockSlim _rwLockCoils = new(); private readonly ReaderWriterLockSlim _rwLockCoils = new();
private readonly ReaderWriterLockSlim _rwLockDiscreteInputs = new(); private readonly ReaderWriterLockSlim _rwLockDiscreteInputs = new();

View File

@@ -1,4 +1,6 @@
using System; #if NET6_0_OR_GREATER
using System;
#endif
namespace AMWD.Protocols.Modbus.Common namespace AMWD.Protocols.Modbus.Common
{ {

View File

@@ -92,11 +92,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadCoils:X2}"; string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadCoils:X2}";
// Starting address // Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes(); var addrBytes = startAddress.ToBigEndianBytes();
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
// Quantity // Quantity
byte[] countBytes = count.ToBigEndianBytes(); var countBytes = count.ToBigEndianBytes();
request += $"{countBytes[0]:X2}{countBytes[1]:X2}"; request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
// LRC // LRC
@@ -151,11 +151,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadDiscreteInputs:X2}"; string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadDiscreteInputs:X2}";
// Starting address // Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes(); var addrBytes = startAddress.ToBigEndianBytes();
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
// Quantity // Quantity
byte[] countBytes = count.ToBigEndianBytes(); var countBytes = count.ToBigEndianBytes();
request += $"{countBytes[0]:X2}{countBytes[1]:X2}"; request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
// LRC // LRC
@@ -209,11 +209,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadHoldingRegisters:X2}"; string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadHoldingRegisters:X2}";
// Starting address // Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes(); var addrBytes = startAddress.ToBigEndianBytes();
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
// Quantity // Quantity
byte[] countBytes = count.ToBigEndianBytes(); var countBytes = count.ToBigEndianBytes();
request += $"{countBytes[0]:X2}{countBytes[1]:X2}"; request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
// LRC // LRC
@@ -264,11 +264,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadInputRegisters:X2}"; string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadInputRegisters:X2}";
// Starting address // Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes(); var addrBytes = startAddress.ToBigEndianBytes();
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
// Quantity // Quantity
byte[] countBytes = count.ToBigEndianBytes(); var countBytes = count.ToBigEndianBytes();
request += $"{countBytes[0]:X2}{countBytes[1]:X2}"; request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
// LRC // LRC
@@ -383,7 +383,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteSingleCoil:X2}"; string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteSingleCoil:X2}";
// Starting address // Starting address
byte[] addrBytes = coil.Address.ToBigEndianBytes(); var addrBytes = coil.Address.ToBigEndianBytes();
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
// Value // Value
@@ -426,7 +426,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteSingleRegister:X2}"; string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteSingleRegister:X2}";
// Starting address // Starting address
byte[] addrBytes = register.Address.ToBigEndianBytes(); var addrBytes = register.Address.ToBigEndianBytes();
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
// Value // Value
@@ -497,11 +497,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteMultipleCoils:X2}"; string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteMultipleCoils:X2}";
// Starting address // Starting address
byte[] addrBytes = firstAddress.ToBigEndianBytes(); var addrBytes = firstAddress.ToBigEndianBytes();
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
// Quantity // Quantity
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
request += $"{countBytes[0]:X2}{countBytes[1]:X2}"; request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
// Byte count // Byte count
@@ -567,11 +567,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteMultipleRegisters:X2}"; string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteMultipleRegisters:X2}";
// Starting address // Starting address
byte[] addrBytes = firstAddress.ToBigEndianBytes(); var addrBytes = firstAddress.ToBigEndianBytes();
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
// Quantity // Quantity
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
request += $"{countBytes[0]:X2}{countBytes[1]:X2}"; request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
// Byte count // Byte count
@@ -675,6 +675,10 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
/// <summary> /// <summary>
/// Calculate LRC for Modbus ASCII. /// Calculate LRC for Modbus ASCII.
/// </summary> /// </summary>
/// <remarks>
/// The LRC calculation algorithm is defined in the Modbus serial line specification.
/// See <see href="https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf">Modbus over Serial Line v1.02</see>, Appendix B, page 38.
/// </remarks>
/// <param name="message">The message chars.</param> /// <param name="message">The message chars.</param>
/// <param name="start">The start index.</param> /// <param name="start">The start index.</param>
/// <param name="length">The number of bytes to calculate.</param> /// <param name="length">The number of bytes to calculate.</param>

View File

@@ -6,11 +6,15 @@ using AMWD.Protocols.Modbus.Common.Contracts;
namespace AMWD.Protocols.Modbus.Common.Protocols namespace AMWD.Protocols.Modbus.Common.Protocols
{ {
/// <summary> /// <summary>
/// Implementation of the Modbus RTU over TCP protocol. /// Implementation of the Modbus RTU over Modbus TCP protocol.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The Modbus RTU over Modbus TCP is rarely used. /// The Modbus RTU over Modbus TCP is rarely used.
/// It is a non-standard variant of Modbus TCP that includes the Modbus RTU CRC at the end of the message. /// It is a non-standard variant:
/// You can define it as RTU message with an additional TCP header
/// or as TCP message with an additional CRC16 checksum at the end (header not included!).
/// <br/>
/// Definition found on <see href="https://www.fernhillsoftware.com/help/drivers/modbus/modbus-protocol.html">Fernhill Software</see>.
/// </remarks> /// </remarks>
public class RtuOverTcpProtocol : IModbusProtocol public class RtuOverTcpProtocol : IModbusProtocol
{ {
@@ -115,12 +119,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.ReadCoils; request[7] = (byte)ModbusFunctionCode.ReadCoils;
// Starting address // Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes(); var addrBytes = startAddress.ToBigEndianBytes();
request[8] = addrBytes[0]; request[8] = addrBytes[0];
request[9] = addrBytes[1]; request[9] = addrBytes[1];
// Quantity // Quantity
byte[] countBytes = count.ToBigEndianBytes(); var countBytes = count.ToBigEndianBytes();
request[10] = countBytes[0]; request[10] = countBytes[0];
request[11] = countBytes[1]; request[11] = countBytes[1];
@@ -178,12 +182,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs; request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
// Starting address // Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes(); var addrBytes = startAddress.ToBigEndianBytes();
request[8] = addrBytes[0]; request[8] = addrBytes[0];
request[9] = addrBytes[1]; request[9] = addrBytes[1];
// Quantity // Quantity
byte[] countBytes = count.ToBigEndianBytes(); var countBytes = count.ToBigEndianBytes();
request[10] = countBytes[0]; request[10] = countBytes[0];
request[11] = countBytes[1]; request[11] = countBytes[1];
@@ -241,12 +245,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters; request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
// Starting address // Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes(); var addrBytes = startAddress.ToBigEndianBytes();
request[8] = addrBytes[0]; request[8] = addrBytes[0];
request[9] = addrBytes[1]; request[9] = addrBytes[1];
// Quantity // Quantity
byte[] countBytes = count.ToBigEndianBytes(); var countBytes = count.ToBigEndianBytes();
request[10] = countBytes[0]; request[10] = countBytes[0];
request[11] = countBytes[1]; request[11] = countBytes[1];
@@ -301,12 +305,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.ReadInputRegisters; request[7] = (byte)ModbusFunctionCode.ReadInputRegisters;
// Starting address // Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes(); var addrBytes = startAddress.ToBigEndianBytes();
request[8] = addrBytes[0]; request[8] = addrBytes[0];
request[9] = addrBytes[1]; request[9] = addrBytes[1];
// Quantity // Quantity
byte[] countBytes = count.ToBigEndianBytes(); var countBytes = count.ToBigEndianBytes();
request[10] = countBytes[0]; request[10] = countBytes[0];
request[11] = countBytes[1]; request[11] = countBytes[1];
@@ -428,7 +432,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
// Function code // Function code
request[7] = (byte)ModbusFunctionCode.WriteSingleCoil; request[7] = (byte)ModbusFunctionCode.WriteSingleCoil;
byte[] addrBytes = coil.Address.ToBigEndianBytes(); var addrBytes = coil.Address.ToBigEndianBytes();
request[8] = addrBytes[0]; request[8] = addrBytes[0];
request[9] = addrBytes[1]; request[9] = addrBytes[1];
@@ -475,7 +479,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
// Function code // Function code
request[7] = (byte)ModbusFunctionCode.WriteSingleRegister; request[7] = (byte)ModbusFunctionCode.WriteSingleRegister;
byte[] addrBytes = register.Address.ToBigEndianBytes(); var addrBytes = register.Address.ToBigEndianBytes();
request[8] = addrBytes[0]; request[8] = addrBytes[0];
request[9] = addrBytes[1]; request[9] = addrBytes[1];
@@ -538,12 +542,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils; request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils;
// Starting address // Starting address
byte[] addrBytes = firstAddress.ToBigEndianBytes(); var addrBytes = firstAddress.ToBigEndianBytes();
request[8] = addrBytes[0]; request[8] = addrBytes[0];
request[9] = addrBytes[1]; request[9] = addrBytes[1];
// Quantity // Quantity
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
request[10] = countBytes[0]; request[10] = countBytes[0];
request[11] = countBytes[1]; request[11] = countBytes[1];
@@ -618,12 +622,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters; request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
// Starting address // Starting address
byte[] addrBytes = firstAddress.ToBigEndianBytes(); var addrBytes = firstAddress.ToBigEndianBytes();
request[8] = addrBytes[0]; request[8] = addrBytes[0];
request[9] = addrBytes[1]; request[9] = addrBytes[1];
// Quantity // Quantity
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
request[10] = countBytes[0]; request[10] = countBytes[0];
request[11] = countBytes[1]; request[11] = countBytes[1];
@@ -743,7 +747,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
// Transaction id // Transaction id
ushort txId = GetNextTransacitonId(); ushort txId = GetNextTransacitonId();
byte[] txBytes = txId.ToBigEndianBytes(); var txBytes = txId.ToBigEndianBytes();
header[0] = txBytes[0]; header[0] = txBytes[0];
header[1] = txBytes[1]; header[1] = txBytes[1];
@@ -752,7 +756,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
header[3] = 0x00; header[3] = 0x00;
// Number of following bytes // Number of following bytes
byte[] countBytes = ((ushort)followingBytes).ToBigEndianBytes(); var countBytes = ((ushort)followingBytes).ToBigEndianBytes();
header[4] = countBytes[0]; header[4] = countBytes[0];
header[5] = countBytes[1]; header[5] = countBytes[1];

View File

@@ -10,6 +10,22 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
/// </summary> /// </summary>
public class RtuProtocol : IModbusProtocol public class RtuProtocol : IModbusProtocol
{ {
#region Fields
private static readonly byte[] _readFunctionCodes = [
(byte)ModbusFunctionCode.ReadCoils,
(byte)ModbusFunctionCode.ReadDiscreteInputs,
(byte)ModbusFunctionCode.ReadHoldingRegisters,
(byte)ModbusFunctionCode.ReadInputRegisters];
private static readonly byte[] _writeFunctionCodes = [
(byte)ModbusFunctionCode.WriteSingleCoil,
(byte)ModbusFunctionCode.WriteSingleRegister,
(byte)ModbusFunctionCode.WriteMultipleCoils,
(byte)ModbusFunctionCode.WriteMultipleRegisters];
#endregion Fields
#region Constants #region Constants
/// <summary> /// <summary>
@@ -96,12 +112,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[1] = (byte)ModbusFunctionCode.ReadCoils; request[1] = (byte)ModbusFunctionCode.ReadCoils;
// Starting address // Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes(); var addrBytes = startAddress.ToBigEndianBytes();
request[2] = addrBytes[0]; request[2] = addrBytes[0];
request[3] = addrBytes[1]; request[3] = addrBytes[1];
// Quantity // Quantity
byte[] countBytes = count.ToBigEndianBytes(); var countBytes = count.ToBigEndianBytes();
request[4] = countBytes[0]; request[4] = countBytes[0];
request[5] = countBytes[1]; request[5] = countBytes[1];
@@ -156,12 +172,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[1] = (byte)ModbusFunctionCode.ReadDiscreteInputs; request[1] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
// Starting address // Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes(); var addrBytes = startAddress.ToBigEndianBytes();
request[2] = addrBytes[0]; request[2] = addrBytes[0];
request[3] = addrBytes[1]; request[3] = addrBytes[1];
// Quantity // Quantity
byte[] countBytes = count.ToBigEndianBytes(); var countBytes = count.ToBigEndianBytes();
request[4] = countBytes[0]; request[4] = countBytes[0];
request[5] = countBytes[1]; request[5] = countBytes[1];
@@ -216,12 +232,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[1] = (byte)ModbusFunctionCode.ReadHoldingRegisters; request[1] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
// Starting address // Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes(); var addrBytes = startAddress.ToBigEndianBytes();
request[2] = addrBytes[0]; request[2] = addrBytes[0];
request[3] = addrBytes[1]; request[3] = addrBytes[1];
// Quantity // Quantity
byte[] countBytes = count.ToBigEndianBytes(); var countBytes = count.ToBigEndianBytes();
request[4] = countBytes[0]; request[4] = countBytes[0];
request[5] = countBytes[1]; request[5] = countBytes[1];
@@ -273,12 +289,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[1] = (byte)ModbusFunctionCode.ReadInputRegisters; request[1] = (byte)ModbusFunctionCode.ReadInputRegisters;
// Starting address // Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes(); var addrBytes = startAddress.ToBigEndianBytes();
request[2] = addrBytes[0]; request[2] = addrBytes[0];
request[3] = addrBytes[1]; request[3] = addrBytes[1];
// Quantity // Quantity
byte[] countBytes = count.ToBigEndianBytes(); var countBytes = count.ToBigEndianBytes();
request[4] = countBytes[0]; request[4] = countBytes[0];
request[5] = countBytes[1]; request[5] = countBytes[1];
@@ -394,7 +410,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
// Function code // Function code
request[1] = (byte)ModbusFunctionCode.WriteSingleCoil; request[1] = (byte)ModbusFunctionCode.WriteSingleCoil;
byte[] addrBytes = coil.Address.ToBigEndianBytes(); var addrBytes = coil.Address.ToBigEndianBytes();
request[2] = addrBytes[0]; request[2] = addrBytes[0];
request[3] = addrBytes[1]; request[3] = addrBytes[1];
@@ -438,7 +454,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
// Function code // Function code
request[1] = (byte)ModbusFunctionCode.WriteSingleRegister; request[1] = (byte)ModbusFunctionCode.WriteSingleRegister;
byte[] addrBytes = register.Address.ToBigEndianBytes(); var addrBytes = register.Address.ToBigEndianBytes();
request[2] = addrBytes[0]; request[2] = addrBytes[0];
request[3] = addrBytes[1]; request[3] = addrBytes[1];
@@ -495,11 +511,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[1] = (byte)ModbusFunctionCode.WriteMultipleCoils; request[1] = (byte)ModbusFunctionCode.WriteMultipleCoils;
byte[] addrBytes = firstAddress.ToBigEndianBytes(); var addrBytes = firstAddress.ToBigEndianBytes();
request[2] = addrBytes[0]; request[2] = addrBytes[0];
request[3] = addrBytes[1]; request[3] = addrBytes[1];
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
request[4] = countBytes[0]; request[4] = countBytes[0];
request[5] = countBytes[1]; request[5] = countBytes[1];
@@ -565,11 +581,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[0] = unitId; request[0] = unitId;
request[1] = (byte)ModbusFunctionCode.WriteMultipleRegisters; request[1] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
byte[] addrBytes = firstAddress.ToBigEndianBytes(); var addrBytes = firstAddress.ToBigEndianBytes();
request[2] = addrBytes[0]; request[2] = addrBytes[0];
request[3] = addrBytes[1]; request[3] = addrBytes[1];
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
request[4] = countBytes[0]; request[4] = countBytes[0];
request[5] = countBytes[1]; request[5] = countBytes[1];
@@ -627,7 +643,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
// - 0x03 Read Holding Registers // - 0x03 Read Holding Registers
// - 0x04 Read Input Registers // - 0x04 Read Input Registers
// do have a "following bytes" at position 3 // do have a "following bytes" at position 3
if (new[] { 0x01, 0x02, 0x03, 0x04 }.Contains(responseBytes[1])) if (_readFunctionCodes.Contains(responseBytes[1]))
{ {
// Unit ID, Function Code, ByteCount, 2x CRC and length of ByteCount // Unit ID, Function Code, ByteCount, 2x CRC and length of ByteCount
if (responseBytes.Count < 5 + responseBytes[2]) if (responseBytes.Count < 5 + responseBytes[2])
@@ -638,7 +654,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
// - 0x06 Write Single Register // - 0x06 Write Single Register
// - 0x0F Write Multiple Coils // - 0x0F Write Multiple Coils
// - 0x10 Write Multiple Registers // - 0x10 Write Multiple Registers
if (new[] { 0x05, 0x06, 0x0F, 0x10 }.Contains(responseBytes[1])) if (_writeFunctionCodes.Contains(responseBytes[1]))
{ {
// Write Single => Unit ID, Function code, 2x Address, 2x Value, 2x CRC // Write Single => Unit ID, Function code, 2x Address, 2x Value, 2x CRC
// Write Multi => Unit ID, Function code, 2x Address, 2x QuantityWritten, 2x CRC // Write Multi => Unit ID, Function code, 2x Address, 2x QuantityWritten, 2x CRC
@@ -715,13 +731,13 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
if (isError) if (isError)
throw new ModbusException("Remote Error") { ErrorCode = (ModbusErrorCode)response[2] }; throw new ModbusException("Remote Error") { ErrorCode = (ModbusErrorCode)response[2] };
if (new[] { 0x01, 0x02, 0x03, 0x04 }.Contains(fnCode)) if (_readFunctionCodes.Contains(fnCode))
{ {
if (response.Count != 5 + response[2]) if (response.Count != 5 + response[2])
throw new ModbusException("Number of following bytes does not match."); throw new ModbusException("Number of following bytes does not match.");
} }
if (new[] { 0x05, 0x06, 0x0F, 0x10 }.Contains(fnCode)) if (_writeFunctionCodes.Contains(fnCode))
{ {
if (response.Count != 8) if (response.Count != 8)
throw new ModbusException("Number of bytes does not match."); throw new ModbusException("Number of bytes does not match.");
@@ -733,6 +749,10 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
/// <summary> /// <summary>
/// Calculate CRC16 for Modbus RTU. /// Calculate CRC16 for Modbus RTU.
/// </summary> /// </summary>
/// <remarks>
/// The CRC 16 calculation algorithm is defined in the Modbus serial line specification.
/// See <see href="https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf">Modbus over Serial Line v1.02</see>, Appendix B, page 40.
/// </remarks>
/// <param name="bytes">The message bytes.</param> /// <param name="bytes">The message bytes.</param>
/// <param name="start">The start index.</param> /// <param name="start">The start index.</param>
/// <param name="length">The number of bytes to calculate.</param> /// <param name="length">The number of bytes to calculate.</param>

View File

@@ -101,12 +101,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.ReadCoils; request[7] = (byte)ModbusFunctionCode.ReadCoils;
// Starting address // Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes(); var addrBytes = startAddress.ToBigEndianBytes();
request[8] = addrBytes[0]; request[8] = addrBytes[0];
request[9] = addrBytes[1]; request[9] = addrBytes[1];
// Quantity // Quantity
byte[] countBytes = count.ToBigEndianBytes(); var countBytes = count.ToBigEndianBytes();
request[10] = countBytes[0]; request[10] = countBytes[0];
request[11] = countBytes[1]; request[11] = countBytes[1];
@@ -159,12 +159,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs; request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
// Starting address // Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes(); var addrBytes = startAddress.ToBigEndianBytes();
request[8] = addrBytes[0]; request[8] = addrBytes[0];
request[9] = addrBytes[1]; request[9] = addrBytes[1];
// Quantity // Quantity
byte[] countBytes = count.ToBigEndianBytes(); var countBytes = count.ToBigEndianBytes();
request[10] = countBytes[0]; request[10] = countBytes[0];
request[11] = countBytes[1]; request[11] = countBytes[1];
@@ -217,12 +217,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters; request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
// Starting address // Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes(); var addrBytes = startAddress.ToBigEndianBytes();
request[8] = addrBytes[0]; request[8] = addrBytes[0];
request[9] = addrBytes[1]; request[9] = addrBytes[1];
// Quantity // Quantity
byte[] countBytes = count.ToBigEndianBytes(); var countBytes = count.ToBigEndianBytes();
request[10] = countBytes[0]; request[10] = countBytes[0];
request[11] = countBytes[1]; request[11] = countBytes[1];
@@ -272,12 +272,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.ReadInputRegisters; request[7] = (byte)ModbusFunctionCode.ReadInputRegisters;
// Starting address // Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes(); var addrBytes = startAddress.ToBigEndianBytes();
request[8] = addrBytes[0]; request[8] = addrBytes[0];
request[9] = addrBytes[1]; request[9] = addrBytes[1];
// Quantity // Quantity
byte[] countBytes = count.ToBigEndianBytes(); var countBytes = count.ToBigEndianBytes();
request[10] = countBytes[0]; request[10] = countBytes[0];
request[11] = countBytes[1]; request[11] = countBytes[1];
@@ -389,7 +389,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
// Function code // Function code
request[7] = (byte)ModbusFunctionCode.WriteSingleCoil; request[7] = (byte)ModbusFunctionCode.WriteSingleCoil;
byte[] addrBytes = coil.Address.ToBigEndianBytes(); var addrBytes = coil.Address.ToBigEndianBytes();
request[8] = addrBytes[0]; request[8] = addrBytes[0];
request[9] = addrBytes[1]; request[9] = addrBytes[1];
@@ -431,7 +431,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
// Function code // Function code
request[7] = (byte)ModbusFunctionCode.WriteSingleRegister; request[7] = (byte)ModbusFunctionCode.WriteSingleRegister;
byte[] addrBytes = register.Address.ToBigEndianBytes(); var addrBytes = register.Address.ToBigEndianBytes();
request[8] = addrBytes[0]; request[8] = addrBytes[0];
request[9] = addrBytes[1]; request[9] = addrBytes[1];
@@ -489,12 +489,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils; request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils;
// Starting address // Starting address
byte[] addrBytes = firstAddress.ToBigEndianBytes(); var addrBytes = firstAddress.ToBigEndianBytes();
request[8] = addrBytes[0]; request[8] = addrBytes[0];
request[9] = addrBytes[1]; request[9] = addrBytes[1];
// Quantity // Quantity
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
request[10] = countBytes[0]; request[10] = countBytes[0];
request[11] = countBytes[1]; request[11] = countBytes[1];
@@ -564,12 +564,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters; request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
// Starting address // Starting address
byte[] addrBytes = firstAddress.ToBigEndianBytes(); var addrBytes = firstAddress.ToBigEndianBytes();
request[8] = addrBytes[0]; request[8] = addrBytes[0];
request[9] = addrBytes[1]; request[9] = addrBytes[1];
// Quantity // Quantity
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
request[10] = countBytes[0]; request[10] = countBytes[0];
request[11] = countBytes[1]; request[11] = countBytes[1];
@@ -678,7 +678,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
// Transaction id // Transaction id
ushort txId = GetNextTransacitonId(); ushort txId = GetNextTransacitonId();
byte[] txBytes = txId.ToBigEndianBytes(); var txBytes = txId.ToBigEndianBytes();
header[0] = txBytes[0]; header[0] = txBytes[0];
header[1] = txBytes[1]; header[1] = txBytes[1];
@@ -687,7 +687,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
header[3] = 0x00; header[3] = 0x00;
// Number of following bytes // Number of following bytes
byte[] countBytes = ((ushort)followingBytes).ToBigEndianBytes(); var countBytes = ((ushort)followingBytes).ToBigEndianBytes();
header[4] = countBytes[0]; header[4] = countBytes[0];
header[5] = countBytes[1]; header[5] = countBytes[1];

View File

@@ -0,0 +1,478 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Protocols.Modbus.Common.Contracts;
using AMWD.Protocols.Modbus.Common.Events;
using AMWD.Protocols.Modbus.Common.Models;
namespace AMWD.Protocols.Modbus.Common.Protocols
{
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal class VirtualProtocol : IModbusProtocol, IDisposable
{
#region Fields
private bool _isDisposed;
private readonly ReaderWriterLockSlim _deviceListLock = new();
private readonly Dictionary<byte, ModbusDevice> _devices = [];
#endregion Fields
public void Dispose()
{
if (_isDisposed)
return;
_isDisposed = true;
_deviceListLock.Dispose();
foreach (var device in _devices.Values)
device.Dispose();
_devices.Clear();
}
#region Events
public event EventHandler<CoilWrittenEventArgs> CoilWritten;
public event EventHandler<RegisterWrittenEventArgs> RegisterWritten;
#endregion Events
#region Properties
public string Name => nameof(VirtualProtocol);
#endregion Properties
#region Protocol
public bool CheckResponseComplete(IReadOnlyList<byte> responseBytes) => true;
public IReadOnlyList<Coil> DeserializeReadCoils(IReadOnlyList<byte> response)
{
if (!_devices.TryGetValue(response[0], out var device))
throw new TimeoutException("Device not found.");
ushort start = response.GetBigEndianUInt16(1);
ushort count = response.GetBigEndianUInt16(3);
return Enumerable.Range(0, count)
.Select(i => device.GetCoil((ushort)(start + i)))
.ToList();
}
public DeviceIdentificationRaw DeserializeReadDeviceIdentification(IReadOnlyList<byte> response)
{
if (!_devices.TryGetValue(response[0], out var _))
throw new TimeoutException("Device not found.");
var result = new DeviceIdentificationRaw
{
AllowsIndividualAccess = false,
MoreRequestsNeeded = false,
Objects = []
};
if (response[1] >= 1)
{
string version = GetType().Assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
.InformationalVersion;
result.Objects.Add(0, Encoding.UTF8.GetBytes("AM.WD"));
result.Objects.Add(1, Encoding.UTF8.GetBytes("AMWD.Protocols.Modbus"));
result.Objects.Add(2, Encoding.UTF8.GetBytes(version));
}
if (response[1] >= 2)
{
result.Objects.Add(3, Encoding.UTF8.GetBytes("https://github.com/AM-WD/AMWD.Protocols.Modbus"));
result.Objects.Add(4, Encoding.UTF8.GetBytes("Modbus Protocol for .NET"));
result.Objects.Add(5, Encoding.UTF8.GetBytes("Virtual Device"));
result.Objects.Add(6, Encoding.UTF8.GetBytes("Virtual Modbus Client"));
}
if (response[1] >= 3)
{
for (int i = 128; i < 256; i++)
result.Objects.Add((byte)i, []);
}
return result;
}
public IReadOnlyList<DiscreteInput> DeserializeReadDiscreteInputs(IReadOnlyList<byte> response)
{
if (!_devices.TryGetValue(response[0], out var device))
throw new TimeoutException("Device not found.");
ushort start = response.GetBigEndianUInt16(1);
ushort count = response.GetBigEndianUInt16(3);
return Enumerable.Range(0, count)
.Select(i => device.GetDiscreteInput((ushort)(start + i)))
.ToList();
}
public IReadOnlyList<HoldingRegister> DeserializeReadHoldingRegisters(IReadOnlyList<byte> response)
{
if (!_devices.TryGetValue(response[0], out var device))
throw new TimeoutException("Device not found.");
ushort start = response.GetBigEndianUInt16(1);
ushort count = response.GetBigEndianUInt16(3);
return Enumerable.Range(0, count)
.Select(i => device.GetHoldingRegister((ushort)(start + i)))
.ToList();
}
public IReadOnlyList<InputRegister> DeserializeReadInputRegisters(IReadOnlyList<byte> response)
{
if (!_devices.TryGetValue(response[0], out var device))
throw new TimeoutException("Device not found.");
ushort start = response.GetBigEndianUInt16(1);
ushort count = response.GetBigEndianUInt16(3);
return Enumerable.Range(0, count)
.Select(i => device.GetInputRegister((ushort)(start + i)))
.ToList();
}
public (ushort FirstAddress, ushort NumberOfCoils) DeserializeWriteMultipleCoils(IReadOnlyList<byte> response)
{
if (!_devices.TryGetValue(response[0], out var device))
throw new TimeoutException("Device not found.");
ushort start = response.GetBigEndianUInt16(1);
ushort count = response.GetBigEndianUInt16(3);
for (int i = 0; i < count; i++)
{
var coil = new Coil
{
Address = (ushort)(start + i),
HighByte = response[5 + i]
};
device.SetCoil(coil);
Task.Run(() =>
{
try
{
CoilWritten?.Invoke(this, new CoilWrittenEventArgs(
unitId: response[0],
address: coil.Address,
value: coil.Value));
}
catch
{
// ignore
}
});
}
return (start, count);
}
public (ushort FirstAddress, ushort NumberOfRegisters) DeserializeWriteMultipleHoldingRegisters(IReadOnlyList<byte> response)
{
if (!_devices.TryGetValue(response[0], out var device))
throw new TimeoutException("Device not found.");
ushort start = response.GetBigEndianUInt16(1);
ushort count = response.GetBigEndianUInt16(3);
for (int i = 0; i < count; i++)
{
var register = new HoldingRegister
{
Address = (ushort)(start + i),
HighByte = response[5 + i * 2],
LowByte = response[5 + i * 2 + 1]
};
device.SetHoldingRegister(register);
Task.Run(() =>
{
try
{
RegisterWritten?.Invoke(this, new RegisterWrittenEventArgs(
unitId: response[0],
address: register.Address,
highByte: register.HighByte,
lowByte: register.LowByte));
}
catch
{
// ignore
}
});
}
return (start, count);
}
public Coil DeserializeWriteSingleCoil(IReadOnlyList<byte> response)
{
if (!_devices.TryGetValue(response[0], out var device))
throw new TimeoutException("Device not found.");
var coil = new Coil
{
Address = response.GetBigEndianUInt16(1),
HighByte = response[3]
};
device.SetCoil(coil);
Task.Run(() =>
{
try
{
CoilWritten?.Invoke(this, new CoilWrittenEventArgs(
unitId: response[0],
address: coil.Address,
value: coil.Value));
}
catch
{
// ignore
}
});
return coil;
}
public HoldingRegister DeserializeWriteSingleHoldingRegister(IReadOnlyList<byte> response)
{
if (!_devices.TryGetValue(response[0], out var device))
throw new TimeoutException("Device not found.");
var register = new HoldingRegister
{
Address = response.GetBigEndianUInt16(1),
HighByte = response[3],
LowByte = response[4]
};
device.SetHoldingRegister(register);
Task.Run(() =>
{
try
{
RegisterWritten?.Invoke(this, new RegisterWrittenEventArgs(
unitId: response[0],
address: register.Address,
highByte: register.HighByte,
lowByte: register.LowByte));
}
catch
{
// ignore
}
});
return register;
}
public IReadOnlyList<byte> SerializeReadCoils(byte unitId, ushort startAddress, ushort count)
{
return [unitId, .. startAddress.ToBigEndianBytes(), .. count.ToBigEndianBytes()];
}
public IReadOnlyList<byte> SerializeReadDeviceIdentification(byte unitId, ModbusDeviceIdentificationCategory category, ModbusDeviceIdentificationObject objectId)
{
if (!Enum.IsDefined(typeof(ModbusDeviceIdentificationCategory), category))
throw new ArgumentOutOfRangeException(nameof(category));
return [unitId, (byte)category];
}
public IReadOnlyList<byte> SerializeReadDiscreteInputs(byte unitId, ushort startAddress, ushort count)
{
return [unitId, .. startAddress.ToBigEndianBytes(), .. count.ToBigEndianBytes()];
}
public IReadOnlyList<byte> SerializeReadHoldingRegisters(byte unitId, ushort startAddress, ushort count)
{
return [unitId, .. startAddress.ToBigEndianBytes(), .. count.ToBigEndianBytes()];
}
public IReadOnlyList<byte> SerializeReadInputRegisters(byte unitId, ushort startAddress, ushort count)
{
return [unitId, .. startAddress.ToBigEndianBytes(), .. count.ToBigEndianBytes()];
}
public IReadOnlyList<byte> SerializeWriteMultipleCoils(byte unitId, IReadOnlyList<Coil> coils)
{
ushort start = coils.OrderBy(c => c.Address).First().Address;
ushort count = (ushort)coils.Count;
byte[] values = coils.Select(c => c.HighByte).ToArray();
return [unitId, .. start.ToBigEndianBytes(), .. count.ToBigEndianBytes(), .. values];
}
public IReadOnlyList<byte> SerializeWriteMultipleHoldingRegisters(byte unitId, IReadOnlyList<HoldingRegister> registers)
{
ushort start = registers.OrderBy(c => c.Address).First().Address;
ushort count = (ushort)registers.Count;
byte[] values = registers.SelectMany(r => new[] { r.HighByte, r.LowByte }).ToArray();
return [unitId, .. start.ToBigEndianBytes(), .. count.ToBigEndianBytes(), .. values];
}
public IReadOnlyList<byte> SerializeWriteSingleCoil(byte unitId, Coil coil)
{
return [unitId, .. coil.Address.ToBigEndianBytes(), coil.HighByte];
}
public IReadOnlyList<byte> SerializeWriteSingleHoldingRegister(byte unitId, HoldingRegister register)
{
return [unitId, .. register.Address.ToBigEndianBytes(), register.HighByte, register.LowByte];
}
public void ValidateResponse(IReadOnlyList<byte> request, IReadOnlyList<byte> response)
{
if (!request.SequenceEqual(response))
throw new InvalidOperationException("Request and response have to be the same on virtual protocol.");
}
#endregion Protocol
#region Device Handling
public bool AddDevice(byte unitId)
{
Assertions();
using (_deviceListLock.GetWriteLock())
{
if (_devices.ContainsKey(unitId))
return false;
_devices.Add(unitId, new ModbusDevice(unitId));
return true;
}
}
public bool RemoveDevice(byte unitId)
{
Assertions();
using (_deviceListLock.GetWriteLock())
{
if (_devices.ContainsKey(unitId))
return false;
return _devices.Remove(unitId);
}
}
#endregion Device Handling
#region Entity Handling
public Coil GetCoil(byte unitId, ushort address)
{
Assertions();
using (_deviceListLock.GetReadLock())
{
return _devices.TryGetValue(unitId, out var device)
? device.GetCoil(address)
: null;
}
}
public void SetCoil(byte unitId, Coil coil)
{
Assertions();
using (_deviceListLock.GetWriteLock())
{
if (_devices.TryGetValue(unitId, out var device))
device.SetCoil(coil);
}
}
public DiscreteInput GetDiscreteInput(byte unitId, ushort address)
{
Assertions();
using (_deviceListLock.GetReadLock())
{
return _devices.TryGetValue(unitId, out var device)
? device.GetDiscreteInput(address)
: null;
}
}
public void SetDiscreteInput(byte unitId, DiscreteInput discreteInput)
{
Assertions();
using (_deviceListLock.GetWriteLock())
{
if (_devices.TryGetValue(unitId, out var device))
device.SetDiscreteInput(discreteInput);
}
}
public HoldingRegister GetHoldingRegister(byte unitId, ushort address)
{
Assertions();
using (_deviceListLock.GetReadLock())
{
return _devices.TryGetValue(unitId, out var device)
? device.GetHoldingRegister(address)
: null;
}
}
public void SetHoldingRegister(byte unitId, HoldingRegister holdingRegister)
{
Assertions();
using (_deviceListLock.GetWriteLock())
{
if (_devices.TryGetValue(unitId, out var device))
device.SetHoldingRegister(holdingRegister);
}
}
public InputRegister GetInputRegister(byte unitId, ushort address)
{
Assertions();
using (_deviceListLock.GetReadLock())
{
return _devices.TryGetValue(unitId, out var device)
? device.GetInputRegister(address)
: null;
}
}
public void SetInputRegister(byte unitId, InputRegister inputRegister)
{
Assertions();
using (_deviceListLock.GetWriteLock())
{
if (_devices.TryGetValue(unitId, out var device))
device.SetInputRegister(inputRegister);
}
}
#endregion Entity Handling
private void Assertions()
{
#if NET8_0_OR_GREATER
ObjectDisposedException.ThrowIf(_isDisposed, this);
#else
if (_isDisposed)
throw new ObjectDisposedException(GetType().Name);
#endif
}
}
}

View File

@@ -50,7 +50,8 @@ The different types handled by the Modbus Protocol.
In addition, you'll find the `DeviceIdentification` there. In addition, you'll find the `DeviceIdentification` there.
It is used for a "special" function called _Read Device Identification_ (0x2B / 43), not supported on all devices. It is used for a "special" function called _Read Device Identification_ (0x2B / 43), not supported on all devices.
The `ModbusDevice` is used for the server implementations in the derived packages. The `ModbusDevice` is used for the `VirtualModbusClient`.
In combination with the *Proxy implementations (in the derived packages) it can be used as server.
### Protocols ### Protocols
@@ -59,8 +60,8 @@ Here you have the specific default implementations for the Modbus Protocol.
- ASCII - ASCII
- RTU - RTU
- RTU over TCP
- TCP - TCP
- [RTU over TCP]
**NOTE:** **NOTE:**
The implementations over serial line (RTU and ASCII) have a minimum unit ID of one (1) and maximum unit ID of 247 referring to the specification. The implementations over serial line (RTU and ASCII) have a minimum unit ID of one (1) and maximum unit ID of 247 referring to the specification.
@@ -68,4 +69,10 @@ This validation is _not_ implemented here due to real world experience, that som
--- ---
Published under MIT License (see [**tl;dr**Legal](https://www.tldrlegal.com/license/mit-license)) Published under MIT License (see [choose a license])
[RTU over TCP]: https://www.fernhillsoftware.com/help/drivers/modbus/modbus-protocol.html
[choose a license]: https://choosealicense.com/licenses/mit/

View File

@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
using AMWD.Protocols.Modbus.Common.Contracts;
using AMWD.Protocols.Modbus.Common.Events;
using AMWD.Protocols.Modbus.Common.Models;
using AMWD.Protocols.Modbus.Common.Protocols;
namespace AMWD.Protocols.Modbus.Common.Utils
{
/// <summary>
/// Implements a virtual Modbus client.
/// </summary>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public class VirtualModbusClient : ModbusClientBase
{
#region Constructor
/// <summary>
/// Initializes a new instance of the <see cref="VirtualModbusClient"/> class.
/// </summary>
/// <remarks><strong>DO NOT MODIFY</strong> connection or protocol.</remarks>
public VirtualModbusClient()
: base(new VirtualConnection())
{
Protocol = new VirtualProtocol();
TypedProtocol.CoilWritten += (sender, e) => CoilWritten?.Invoke(this, e);
TypedProtocol.RegisterWritten += (sender, e) => RegisterWritten?.Invoke(this, e);
}
#endregion Constructor
#region Events
/// <summary>
/// Indicates that a <see cref="Coil"/>-value received through a remote client has been written.
/// </summary>
public event EventHandler<CoilWrittenEventArgs> CoilWritten;
/// <summary>
/// Indicates that a <see cref="HoldingRegister"/>-value received from a remote client has been written.
/// </summary>
public event EventHandler<RegisterWrittenEventArgs> RegisterWritten;
#endregion Events
#region Properties
internal VirtualProtocol TypedProtocol
=> Protocol as VirtualProtocol;
#endregion Properties
#region Device Handling
/// <summary>
/// Adds a device to the virtual client.
/// </summary>
/// <param name="unitId">The unit id of the device.</param>
/// <returns><see langword="true"/> if the device was added successfully, <see langword="false"/> otherwise.</returns>
public bool AddDevice(byte unitId)
=> TypedProtocol.AddDevice(unitId);
/// <summary>
/// Removes a device from the virtual client.
/// </summary>
/// <param name="unitId">The unit id of the device.</param>
/// <returns><see langword="true"/> if the device was removed successfully, <see langword="false"/> otherwise.</returns>
public bool RemoveDevice(byte unitId)
=> TypedProtocol.RemoveDevice(unitId);
#endregion Device Handling
#region Entity Handling
/// <summary>
/// Gets a <see cref="Coil"/> from the specified <see cref="ModbusDevice"/>.
/// </summary>
/// <param name="unitId">The unit ID of the device.</param>
/// <param name="address">The address of the <see cref="Coil"/>.</param>
public Coil GetCoil(byte unitId, ushort address)
=> TypedProtocol.GetCoil(unitId, address);
/// <summary>
/// Sets a <see cref="Coil"/> to the specified <see cref="ModbusDevice"/>.
/// </summary>
/// <param name="unitId">The unit ID of the device.</param>
/// <param name="coil">The <see cref="Coil"/> to set.</param>
public void SetCoil(byte unitId, Coil coil)
=> TypedProtocol.SetCoil(unitId, coil);
/// <summary>
/// Gets a <see cref="DiscreteInput"/> from the specified <see cref="ModbusDevice"/>.
/// </summary>
/// <param name="unitId">The unit ID of the device.</param>
/// <param name="address">The address of the <see cref="DiscreteInput"/>.</param>
public DiscreteInput GetDiscreteInput(byte unitId, ushort address)
=> TypedProtocol.GetDiscreteInput(unitId, address);
/// <summary>
/// Sets a <see cref="DiscreteInput"/> to the specified <see cref="ModbusDevice"/>.
/// </summary>
/// <param name="unitId">The unit ID of the device.</param>
/// <param name="discreteInput">The <see cref="DiscreteInput"/> to set.</param>
public void SetDiscreteInput(byte unitId, DiscreteInput discreteInput)
=> TypedProtocol.SetDiscreteInput(unitId, discreteInput);
/// <summary>
/// Gets a <see cref="HoldingRegister"/> from the specified <see cref="ModbusDevice"/>.
/// </summary>
/// <param name="unitId">The unit ID of the device.</param>
/// <param name="address">The address of the <see cref="HoldingRegister"/>.</param>
public HoldingRegister GetHoldingRegister(byte unitId, ushort address)
=> TypedProtocol.GetHoldingRegister(unitId, address);
/// <summary>
/// Sets a <see cref="HoldingRegister"/> to the specified <see cref="ModbusDevice"/>.
/// </summary>
/// <param name="unitId">The unit ID of the device.</param>
/// <param name="holdingRegister">The <see cref="HoldingRegister"/> to set.</param>
public void SetHoldingRegister(byte unitId, HoldingRegister holdingRegister)
=> TypedProtocol.SetHoldingRegister(unitId, holdingRegister);
/// <summary>
/// Gets a <see cref="InputRegister"/> from the specified <see cref="ModbusDevice"/>.
/// </summary>
/// <param name="unitId">The unit ID of the device.</param>
/// <param name="address">The address of the <see cref="InputRegister"/>.</param>
public InputRegister GetInputRegister(byte unitId, ushort address)
=> TypedProtocol.GetInputRegister(unitId, address);
/// <summary>
/// Sets a <see cref="InputRegister"/> to the specified <see cref="ModbusDevice"/>.
/// </summary>
/// <param name="unitId">The unit ID of the device.</param>
/// <param name="inputRegister">The <see cref="InputRegister"/> to set.</param>
public void SetInputRegister(byte unitId, InputRegister inputRegister)
=> TypedProtocol.SetInputRegister(unitId, inputRegister);
#endregion Entity Handling
#region Methods
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
TypedProtocol.Dispose();
base.Dispose(disposing);
}
#endregion Methods
#region Connection
internal class VirtualConnection : IModbusConnection
{
public string Name => nameof(VirtualConnection);
public TimeSpan IdleTimeout { get; set; }
public TimeSpan ConnectTimeout { get; set; }
public TimeSpan ReadTimeout { get; set; }
public TimeSpan WriteTimeout { get; set; }
public void Dispose()
{ /* nothing to do */ }
public Task<IReadOnlyList<byte>> InvokeAsync(
IReadOnlyList<byte> request,
Func<IReadOnlyList<byte>, bool> validateResponseComplete,
CancellationToken cancellationToken = default) => Task.FromResult(request);
}
#endregion Connection
}
}

View File

@@ -1,9 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks> <TargetFrameworks>netstandard2.0;net6.0;net8.0;net10.0</TargetFrameworks>
<LangVersion>12.0</LangVersion>
<PackageId>AMWD.Protocols.Modbus.Serial</PackageId> <PackageId>AMWD.Protocols.Modbus.Serial</PackageId>
<AssemblyName>amwd-modbus-serial</AssemblyName> <AssemblyName>amwd-modbus-serial</AssemblyName>
<RootNamespace>AMWD.Protocols.Modbus.Serial</RootNamespace> <RootNamespace>AMWD.Protocols.Modbus.Serial</RootNamespace>
@@ -14,17 +12,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="../AMWD.Protocols.Modbus.Common/InternalsVisibleTo.cs" Link="InternalsVisibleTo.cs" />
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" /> <Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" />
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ReaderWriterLockSlimExtensions.cs" Link="Extensions/ReaderWriterLockSlimExtensions.cs" /> <Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ReaderWriterLockSlimExtensions.cs" Link="Extensions/ReaderWriterLockSlimExtensions.cs" />
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/AsyncQueue.cs" Link="Utils/AsyncQueue.cs" /> <Compile Include="../AMWD.Protocols.Modbus.Common/Utils/AsyncQueue.cs" Link="Utils/AsyncQueue.cs" />
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/RequestQueueItem.cs" Link="Utils/RequestQueueItem.cs" /> <Compile Include="../AMWD.Protocols.Modbus.Common/Utils/RequestQueueItem.cs" Link="Utils/RequestQueueItem.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="/" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'"> <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="System.IO.Ports" Version="4.7.0" /> <PackageReference Include="System.IO.Ports" Version="4.7.0" />
</ItemGroup> </ItemGroup>
@@ -37,12 +30,12 @@
<PackageReference Include="System.IO.Ports" Version="8.0.0" /> <PackageReference Include="System.IO.Ports" Version="8.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
<PackageReference Include="System.IO.Ports" Version="10.0.0" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" /> <ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Extensions\" />
</ItemGroup>
</Project> </Project>

View File

@@ -8,20 +8,20 @@ using System.Threading.Tasks;
using AMWD.Protocols.Modbus.Common; using AMWD.Protocols.Modbus.Common;
using AMWD.Protocols.Modbus.Common.Contracts; using AMWD.Protocols.Modbus.Common.Contracts;
using AMWD.Protocols.Modbus.Common.Protocols; using AMWD.Protocols.Modbus.Common.Protocols;
using AMWD.Protocols.Modbus.Serial; using AMWD.Protocols.Modbus.Serial.Utils;
namespace AMWD.Protocols.Modbus.Proxy namespace AMWD.Protocols.Modbus.Serial
{ {
/// <summary> /// <summary>
/// Implements a Modbus serial line RTU server proxying all requests to a Modbus client of choice. /// Implements a Modbus serial line RTU server proxying all requests to a Modbus client of choice.
/// </summary> /// </summary>
public class ModbusRtuProxy : IDisposable public class ModbusRtuProxy : IModbusProxy
{ {
#region Fields #region Fields
private bool _isDisposed; private bool _isDisposed;
private readonly SerialPort _serialPort; private readonly SerialPortWrapper _serialPort;
private CancellationTokenSource _stopCts; private CancellationTokenSource _stopCts;
#endregion Fields #endregion Fields
@@ -33,31 +33,25 @@ namespace AMWD.Protocols.Modbus.Proxy
/// </summary> /// </summary>
/// <param name="client">The <see cref="ModbusClientBase"/> used to request the remote device, that should be proxied.</param> /// <param name="client">The <see cref="ModbusClientBase"/> used to request the remote device, that should be proxied.</param>
/// <param name="portName">The name of the serial port to use.</param> /// <param name="portName">The name of the serial port to use.</param>
/// <param name="baudRate">The baud rate of the serial port (Default: 19.200).</param> public ModbusRtuProxy(ModbusClientBase client, string portName)
public ModbusRtuProxy(ModbusClientBase client, string portName, BaudRate baudRate = BaudRate.Baud19200)
{ {
Client = client ?? throw new ArgumentNullException(nameof(client)); Client = client ?? throw new ArgumentNullException(nameof(client));
if (string.IsNullOrWhiteSpace(portName)) if (string.IsNullOrWhiteSpace(portName))
throw new ArgumentNullException(nameof(portName)); throw new ArgumentNullException(nameof(portName));
if (!Enum.IsDefined(typeof(BaudRate), baudRate)) _serialPort = new SerialPortWrapper
throw new ArgumentOutOfRangeException(nameof(baudRate));
if (!ModbusSerialClient.AvailablePortNames.Contains(portName))
throw new ArgumentException($"The serial port ({portName}) is not available.", nameof(portName));
_serialPort = new SerialPort
{ {
PortName = portName, PortName = portName,
BaudRate = (int)baudRate,
Handshake = Handshake.None, BaudRate = (int)BaudRate.Baud19200,
DataBits = 8, DataBits = 8,
ReadTimeout = 1000,
RtsEnable = false,
StopBits = StopBits.One, StopBits = StopBits.One,
Parity = Parity.Even,
Handshake = Handshake.None,
ReadTimeout = 1000,
WriteTimeout = 1000, WriteTimeout = 1000,
Parity = Parity.Even RtsEnable = false,
}; };
} }
@@ -70,80 +64,108 @@ namespace AMWD.Protocols.Modbus.Proxy
/// </summary> /// </summary>
public ModbusClientBase Client { get; } public ModbusClientBase Client { get; }
/// <inheritdoc cref="SerialPort.PortName"/> #region SerialPort Properties
public string PortName => _serialPort.PortName;
/// <summary> /// <inheritdoc cref="SerialPort.PortName" />
/// Gets or sets the baud rate of the serial port. public virtual string PortName
/// </summary> {
public BaudRate BaudRate get => _serialPort.PortName;
set => _serialPort.PortName = value;
}
/// <inheritdoc cref="SerialPort.BaudRate" />
public virtual BaudRate BaudRate
{ {
get => (BaudRate)_serialPort.BaudRate; get => (BaudRate)_serialPort.BaudRate;
set => _serialPort.BaudRate = (int)value; set => _serialPort.BaudRate = (int)value;
} }
/// <inheritdoc cref="SerialPort.Handshake"/>
public Handshake Handshake
{
get => _serialPort.Handshake;
set => _serialPort.Handshake = value;
}
/// <inheritdoc cref="SerialPort.DataBits" /> /// <inheritdoc cref="SerialPort.DataBits" />
public int DataBits /// <remarks>
/// From the Specs:
/// <br/>
/// On <see cref="AsciiProtocol"/> it can be 7 or 8.
/// <br/>
/// On <see cref="RtuProtocol"/> it has to be 8.
/// </remarks>
public virtual int DataBits
{ {
get => _serialPort.DataBits; get => _serialPort.DataBits;
set => _serialPort.DataBits = value; set => _serialPort.DataBits = value;
} }
/// <inheritdoc cref="SerialPort.IsOpen"/> /// <inheritdoc cref="SerialPort.Handshake" />
public bool IsOpen => _serialPort.IsOpen; public virtual Handshake Handshake
/// <summary>
/// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a read operation does not finish.
/// </summary>
public TimeSpan ReadTimeout
{ {
get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout); get => _serialPort.Handshake;
set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds; set => _serialPort.Handshake = value;
}
/// <inheritdoc cref="SerialPort.Parity" />
/// <remarks>
/// From the Specs:
/// <br/>
/// <see cref="Parity.Even"/> is recommended and therefore the default value.
/// <br/>
/// If you use <see cref="Parity.None"/>, <see cref="StopBits.Two"/> is required,
/// otherwise <see cref="StopBits.One"/> should work fine.
/// </remarks>
public virtual Parity Parity
{
get => _serialPort.Parity;
set => _serialPort.Parity = value;
} }
/// <inheritdoc cref="SerialPort.RtsEnable" /> /// <inheritdoc cref="SerialPort.RtsEnable" />
public bool RtsEnable public virtual bool RtsEnable
{ {
get => _serialPort.RtsEnable; get => _serialPort.RtsEnable;
set => _serialPort.RtsEnable = value; set => _serialPort.RtsEnable = value;
} }
/// <inheritdoc cref="SerialPort.StopBits" /> /// <inheritdoc cref="SerialPort.StopBits" />
public StopBits StopBits /// <remarks>
/// From the Specs:
/// <br/>
/// Should be <see cref="StopBits.One"/> for <see cref="Parity.Even"/> or <see cref="Parity.Odd"/>.
/// <br/>
/// Should be <see cref="StopBits.Two"/> for <see cref="Parity.None"/>.
/// </remarks>
public virtual StopBits StopBits
{ {
get => _serialPort.StopBits; get => _serialPort.StopBits;
set => _serialPort.StopBits = value; set => _serialPort.StopBits = value;
} }
/// <inheritdoc cref="SerialPortWrapper.IsOpen"/>
public bool IsOpen => _serialPort.IsOpen;
/// <summary> /// <summary>
/// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a write operation does not finish. /// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a read/receive operation does not finish.
/// </summary> /// </summary>
public TimeSpan WriteTimeout public virtual TimeSpan ReadTimeout
{
get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout);
set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds;
}
/// <summary>
/// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a write/send operation does not finish.
/// </summary>
public virtual TimeSpan WriteTimeout
{ {
get => TimeSpan.FromMilliseconds(_serialPort.WriteTimeout); get => TimeSpan.FromMilliseconds(_serialPort.WriteTimeout);
set => _serialPort.WriteTimeout = (int)value.TotalMilliseconds; set => _serialPort.WriteTimeout = (int)value.TotalMilliseconds;
} }
/// <inheritdoc cref="SerialPort.Parity"/> #endregion SerialPort Properties
public Parity Parity
{
get => _serialPort.Parity;
set => _serialPort.Parity = value;
}
#endregion Properties #endregion Properties
#region Control Methods #region Control Methods
/// <summary> /// <summary>
/// Starts the server. /// Starts the proxy.
/// </summary> /// </summary>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param> /// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public Task StartAsync(CancellationToken cancellationToken = default) public Task StartAsync(CancellationToken cancellationToken = default)
@@ -164,23 +186,22 @@ namespace AMWD.Protocols.Modbus.Proxy
} }
/// <summary> /// <summary>
/// Stops the server. /// Stops the proxy.
/// </summary> /// </summary>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param> /// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public Task StopAsync(CancellationToken cancellationToken = default) public Task StopAsync(CancellationToken cancellationToken = default)
{ {
Assertions(); Assertions();
return StopAsyncInternal(cancellationToken); StopAsyncInternal();
return Task.CompletedTask;
} }
private Task StopAsyncInternal(CancellationToken cancellationToken) private void StopAsyncInternal()
{ {
_stopCts.Cancel(); _stopCts?.Cancel();
_serialPort.Close(); _serialPort.Close();
_serialPort.DataReceived -= OnDataReceived; _serialPort.DataReceived -= OnDataReceived;
return Task.CompletedTask;
} }
/// <summary> /// <summary>
@@ -193,7 +214,7 @@ namespace AMWD.Protocols.Modbus.Proxy
_isDisposed = true; _isDisposed = true;
StopAsyncInternal(CancellationToken.None).Wait(); StopAsyncInternal();
_serialPort.Dispose(); _serialPort.Dispose();
_stopCts?.Dispose(); _stopCts?.Dispose();
@@ -207,13 +228,16 @@ namespace AMWD.Protocols.Modbus.Proxy
if (_isDisposed) if (_isDisposed)
throw new ObjectDisposedException(GetType().FullName); throw new ObjectDisposedException(GetType().FullName);
#endif #endif
if (string.IsNullOrWhiteSpace(PortName))
throw new ArgumentNullException(nameof(PortName), "The serial port name cannot be empty.");
} }
#endregion Control Methods #endregion Control Methods
#region Client Handling #region Client Handling
private void OnDataReceived(object _, SerialDataReceivedEventArgs evArgs) private void OnDataReceived(object _, SerialDataReceivedEventArgs __)
{ {
try try
{ {
@@ -282,16 +306,14 @@ namespace AMWD.Protocols.Modbus.Proxy
default: // unknown function default: // unknown function
{ {
byte[] responseBytes = new byte[5]; var responseBytes = new List<byte>();
Array.Copy(requestBytes, 0, responseBytes, 0, 2); responseBytes.AddRange(requestBytes.Take(2));
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
// Mark as error // Mark as error
responseBytes[1] |= 0x80; responseBytes[1] |= 0x80;
responseBytes[2] = (byte)ModbusErrorCode.IllegalFunction; return ReturnResponse(responseBytes);
SetCrc(responseBytes);
return responseBytes;
} }
} }
} }
@@ -309,7 +331,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.AddRange(requestBytes.Take(2)); responseBytes.AddRange(requestBytes.Take(2));
try try
{ {
var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken); var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)]; byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)];
for (int i = 0; i < coils.Count; i++) for (int i = 0; i < coils.Count; i++)
@@ -332,8 +354,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
private async Task<byte[]> HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -349,7 +370,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.AddRange(requestBytes.Take(2)); responseBytes.AddRange(requestBytes.Take(2));
try try
{ {
var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken); var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)]; byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)];
for (int i = 0; i < discreteInputs.Count; i++) for (int i = 0; i < discreteInputs.Count; i++)
@@ -372,8 +393,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
private async Task<byte[]> HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -389,7 +409,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.AddRange(requestBytes.Take(2)); responseBytes.AddRange(requestBytes.Take(2));
try try
{ {
var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken); var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
byte[] values = new byte[holdingRegisters.Count * 2]; byte[] values = new byte[holdingRegisters.Count * 2];
for (int i = 0; i < holdingRegisters.Count; i++) for (int i = 0; i < holdingRegisters.Count; i++)
@@ -407,8 +427,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
private async Task<byte[]> HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -424,7 +443,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.AddRange(requestBytes.Take(2)); responseBytes.AddRange(requestBytes.Take(2));
try try
{ {
var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken); var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
byte[] values = new byte[count * 2]; byte[] values = new byte[count * 2];
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
@@ -442,8 +461,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
private async Task<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -461,8 +479,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes[1] |= 0x80; responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
try try
@@ -474,7 +491,7 @@ namespace AMWD.Protocols.Modbus.Proxy
LowByte = requestBytes[5], LowByte = requestBytes[5],
}; };
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[0], coil, cancellationToken); bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[0], coil, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (isSuccess) if (isSuccess)
{ {
// Response is an echo of the request // Response is an echo of the request
@@ -492,8 +509,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
private async Task<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -514,7 +530,7 @@ namespace AMWD.Protocols.Modbus.Proxy
LowByte = requestBytes[5] LowByte = requestBytes[5]
}; };
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[0], register, cancellationToken); bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[0], register, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (isSuccess) if (isSuccess)
{ {
// Response is an echo of the request // Response is an echo of the request
@@ -532,8 +548,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
private async Task<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -553,8 +568,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes[1] |= 0x80; responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
try try
@@ -576,7 +590,7 @@ namespace AMWD.Protocols.Modbus.Proxy
}); });
} }
bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[0], coils, cancellationToken); bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[0], coils, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (isSuccess) if (isSuccess)
{ {
// Response is an echo of the request // Response is an echo of the request
@@ -594,8 +608,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
private async Task<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -604,7 +617,7 @@ namespace AMWD.Protocols.Modbus.Proxy
return null; return null;
var responseBytes = new List<byte>(); var responseBytes = new List<byte>();
responseBytes.AddRange(requestBytes.Take(8)); responseBytes.AddRange(requestBytes.Take(2));
ushort firstAddress = requestBytes.GetBigEndianUInt16(2); ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
ushort count = requestBytes.GetBigEndianUInt16(4); ushort count = requestBytes.GetBigEndianUInt16(4);
@@ -615,8 +628,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes[1] |= 0x80; responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
try try
@@ -633,8 +645,9 @@ namespace AMWD.Protocols.Modbus.Proxy
HighByte = requestBytes[baseOffset + i * 2], HighByte = requestBytes[baseOffset + i * 2],
LowByte = requestBytes[baseOffset + i * 2 + 1] LowByte = requestBytes[baseOffset + i * 2 + 1]
}); });
}
bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[0], list, cancellationToken); bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[0], list, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (isSuccess) if (isSuccess)
{ {
// Response is an echo of the request // Response is an echo of the request
@@ -646,19 +659,20 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
} }
}
catch catch
{ {
responseBytes[1] |= 0x80; responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
private async Task<byte[]> HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken)
{ {
if (requestBytes.Length < 7)
return null;
var responseBytes = new List<byte>(); var responseBytes = new List<byte>();
responseBytes.AddRange(requestBytes.Take(2)); responseBytes.AddRange(requestBytes.Take(2));
@@ -667,8 +681,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes[1] |= 0x80; responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction); responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
var firstObject = (ModbusDeviceIdentificationObject)requestBytes[4]; var firstObject = (ModbusDeviceIdentificationObject)requestBytes[4];
@@ -677,8 +690,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes[1] |= 0x80; responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress); responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
var category = (ModbusDeviceIdentificationCategory)requestBytes[3]; var category = (ModbusDeviceIdentificationCategory)requestBytes[3];
@@ -687,13 +699,12 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes[1] |= 0x80; responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
try try
{ {
var res = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken); var deviceInfo = await Client.ReadDeviceIdentificationAsync(requestBytes[0], category, firstObject, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
var bodyBytes = new List<byte>(); var bodyBytes = new List<byte>();
@@ -702,31 +713,20 @@ namespace AMWD.Protocols.Modbus.Proxy
// Conformity // Conformity
bodyBytes.Add((byte)category); bodyBytes.Add((byte)category);
if (res.IsIndividualAccessAllowed) if (deviceInfo.IsIndividualAccessAllowed)
bodyBytes[2] |= 0x80; bodyBytes[2] |= 0x80;
// More, NextId, NumberOfObjects // More, NextId, NumberOfObjects
bodyBytes.AddRange(new byte[3]); bodyBytes.AddRange(new byte[3]);
int maxObjectId; int maxObjectId = category switch
switch (category)
{ {
case ModbusDeviceIdentificationCategory.Basic: ModbusDeviceIdentificationCategory.Basic => 0x02,
maxObjectId = 0x02; ModbusDeviceIdentificationCategory.Regular => 0x06,
break; ModbusDeviceIdentificationCategory.Extended => 0xFF,
// Individual
case ModbusDeviceIdentificationCategory.Regular: _ => requestBytes[4],
maxObjectId = 0x06; };
break;
case ModbusDeviceIdentificationCategory.Extended:
maxObjectId = 0xFF;
break;
default: // Individual
maxObjectId = requestBytes[4];
break;
}
byte numberOfObjects = 0; byte numberOfObjects = 0;
for (int i = requestBytes[4]; i <= maxObjectId; i++) for (int i = requestBytes[4]; i <= maxObjectId; i++)
@@ -735,17 +735,19 @@ namespace AMWD.Protocols.Modbus.Proxy
if (0x07 <= i && i <= 0x7F) if (0x07 <= i && i <= 0x7F)
continue; continue;
byte[] objBytes = GetDeviceObject((byte)i, res); byte[] objBytes = GetDeviceObject((byte)i, deviceInfo);
// We need to split the response if it would exceed the max ADU size // We need to split the response if it would exceed the max ADU size.
if (responseBytes.Count + bodyBytes.Count + objBytes.Length > RtuProtocol.MAX_ADU_LENGTH) // 2 bytes of CRC have to be added.
if (responseBytes.Count + bodyBytes.Count + objBytes.Length + 2 > RtuProtocol.MAX_ADU_LENGTH)
{ {
bodyBytes[3] = 0xFF; bodyBytes[3] = 0xFF;
bodyBytes[4] = (byte)i; bodyBytes[4] = (byte)i;
bodyBytes[5] = numberOfObjects; bodyBytes[5] = numberOfObjects;
responseBytes.AddRange(bodyBytes); responseBytes.AddRange(bodyBytes);
return [.. responseBytes];
return ReturnResponse(responseBytes);
} }
bodyBytes.AddRange(objBytes); bodyBytes.AddRange(objBytes);
@@ -755,16 +757,14 @@ namespace AMWD.Protocols.Modbus.Proxy
bodyBytes[5] = numberOfObjects; bodyBytes[5] = numberOfObjects;
responseBytes.AddRange(bodyBytes); responseBytes.AddRange(bodyBytes);
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
catch catch
{ {
responseBytes[1] |= 0x80; responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
} }
@@ -775,7 +775,7 @@ namespace AMWD.Protocols.Modbus.Proxy
{ {
case ModbusDeviceIdentificationObject.VendorName: case ModbusDeviceIdentificationObject.VendorName:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -783,7 +783,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.ProductCode: case ModbusDeviceIdentificationObject.ProductCode:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -791,7 +791,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.MajorMinorRevision: case ModbusDeviceIdentificationObject.MajorMinorRevision:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -799,7 +799,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.VendorUrl: case ModbusDeviceIdentificationObject.VendorUrl:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -807,7 +807,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.ProductName: case ModbusDeviceIdentificationObject.ProductName:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -815,7 +815,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.ModelName: case ModbusDeviceIdentificationObject.ModelName:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -823,7 +823,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.UserApplicationName: case ModbusDeviceIdentificationObject.UserApplicationName:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -831,9 +831,8 @@ namespace AMWD.Protocols.Modbus.Proxy
default: default:
{ {
if (deviceIdentification.ExtendedObjects.ContainsKey(objectId)) if (deviceIdentification.ExtendedObjects.TryGetValue(objectId, out byte[] bytes))
{ {
byte[] bytes = deviceIdentification.ExtendedObjects[objectId];
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -848,20 +847,28 @@ namespace AMWD.Protocols.Modbus.Proxy
return [.. result]; return [.. result];
} }
private static void SetCrc(byte[] bytes) private static byte[] ReturnResponse(List<byte> response)
{ {
byte[] crc = RtuProtocol.CRC16(bytes, 0, bytes.Length - 2); response.AddRange(RtuProtocol.CRC16(response));
bytes[bytes.Length - 2] = crc[0]; return [.. response];
bytes[bytes.Length - 1] = crc[1];
}
private static void AddCrc(List<byte> bytes)
{
byte[] crc = RtuProtocol.CRC16(bytes);
bytes.Add(crc[0]);
bytes.Add(crc[1]);
} }
#endregion Request Handling #endregion Request Handling
/// <inheritdoc/>
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine($"RTU Proxy");
sb.AppendLine($" {nameof(PortName)}: {PortName}");
sb.AppendLine($" {nameof(BaudRate)}: {(int)BaudRate}");
sb.AppendLine($" {nameof(DataBits)}: {DataBits}");
sb.AppendLine($" {nameof(StopBits)}: {StopBits}");
sb.AppendLine($" {nameof(Parity)}: {Parity}");
sb.AppendLine($" {nameof(Client)}: {Client.GetType().Name}");
return sb.ToString();
}
} }
} }

View File

@@ -0,0 +1,94 @@
using System;
using AMWD.Protocols.Modbus.Common;
using AMWD.Protocols.Modbus.Common.Events;
using AMWD.Protocols.Modbus.Common.Utils;
namespace AMWD.Protocols.Modbus.Serial
{
/// <summary>
/// Implements a Modbus serial line RTU server proxying all requests to a virtual Modbus client.
/// </summary>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public class ModbusRtuServer : ModbusRtuProxy
{
/// <summary>
/// Initializes a new instance of the <see cref="ModbusRtuServer"/> class.
/// </summary>
/// <param name="portName">The name of the serial port to use.</param>
public ModbusRtuServer(string portName)
: base(new VirtualModbusClient(), portName)
{
TypedClient.CoilWritten += (sender, e) => CoilWritten?.Invoke(this, e);
TypedClient.RegisterWritten += (sender, e) => RegisterWritten?.Invoke(this, e);
}
#region Events
/// <summary>
/// Indicates that a <see cref="Coil"/>-value received through a remote client has been written.
/// </summary>
public event EventHandler<CoilWrittenEventArgs> CoilWritten;
/// <summary>
/// Indicates that a <see cref="HoldingRegister"/>-value received from a remote client has been written.
/// </summary>
public event EventHandler<RegisterWrittenEventArgs> RegisterWritten;
#endregion Events
#region Properties
internal VirtualModbusClient TypedClient
=> Client as VirtualModbusClient;
#endregion Properties
#region Device Handling
/// <inheritdoc cref="VirtualModbusClient.AddDevice(byte)"/>
public bool AddDevice(byte unitId)
=> TypedClient.AddDevice(unitId);
/// <inheritdoc cref="VirtualModbusClient.RemoveDevice(byte)"/>
public bool RemoveDevice(byte unitId)
=> TypedClient.RemoveDevice(unitId);
#endregion Device Handling
#region Entity Handling
/// <inheritdoc cref="VirtualModbusClient.GetCoil(byte, ushort)"/>
public Coil GetCoil(byte unitId, ushort address)
=> TypedClient.GetCoil(unitId, address);
/// <inheritdoc cref="VirtualModbusClient.SetCoil(byte, Coil)"/>
public void SetCoil(byte unitId, Coil coil)
=> TypedClient.SetCoil(unitId, coil);
/// <inheritdoc cref="VirtualModbusClient.GetDiscreteInput(byte, ushort)"/>
public DiscreteInput GetDiscreteInput(byte unitId, ushort address)
=> TypedClient.GetDiscreteInput(unitId, address);
/// <inheritdoc cref="VirtualModbusClient.SetDiscreteInput(byte, DiscreteInput)"/>
public void SetDiscreteInput(byte unitId, DiscreteInput discreteInput)
=> TypedClient.SetDiscreteInput(unitId, discreteInput);
/// <inheritdoc cref="VirtualModbusClient.GetHoldingRegister(byte, ushort)"/>
public HoldingRegister GetHoldingRegister(byte unitId, ushort address)
=> TypedClient.GetHoldingRegister(unitId, address);
/// <inheritdoc cref="VirtualModbusClient.SetHoldingRegister(byte, HoldingRegister)"/>
public void SetHoldingRegister(byte unitId, HoldingRegister holdingRegister)
=> TypedClient.SetHoldingRegister(unitId, holdingRegister);
/// <inheritdoc cref="VirtualModbusClient.GetInputRegister(byte, ushort)"/>
public InputRegister GetInputRegister(byte unitId, ushort address)
=> TypedClient.GetInputRegister(unitId, address);
/// <inheritdoc cref="VirtualModbusClient.SetInputRegister(byte, InputRegister)"/>
public void SetInputRegister(byte unitId, InputRegister inputRegister)
=> TypedClient.SetInputRegister(unitId, inputRegister);
#endregion Entity Handling
}
}

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.IO.Ports; using System.IO.Ports;
using System.Text;
using AMWD.Protocols.Modbus.Common.Contracts; using AMWD.Protocols.Modbus.Common.Contracts;
using AMWD.Protocols.Modbus.Common.Protocols; using AMWD.Protocols.Modbus.Common.Protocols;
@@ -15,7 +16,7 @@ namespace AMWD.Protocols.Modbus.Serial
/// </summary> /// </summary>
/// <param name="portName">The name of the serial port to use.</param> /// <param name="portName">The name of the serial port to use.</param>
public ModbusSerialClient(string portName) public ModbusSerialClient(string portName)
: this(new ModbusSerialConnection { PortName = portName }) : this(new ModbusSerialConnection(portName))
{ } { }
/// <summary> /// <summary>
@@ -40,8 +41,8 @@ namespace AMWD.Protocols.Modbus.Serial
Protocol = new RtuProtocol(); Protocol = new RtuProtocol();
} }
/// <inheritdoc cref="SerialPort.GetPortNames" /> /// <inheritdoc cref="ModbusSerialConnection.AvailablePortNames" />
public static string[] AvailablePortNames => SerialPort.GetPortNames(); public static string[] AvailablePortNames => ModbusSerialConnection.AvailablePortNames;
/// <inheritdoc cref="IModbusConnection.IdleTimeout"/> /// <inheritdoc cref="IModbusConnection.IdleTimeout"/>
public TimeSpan IdleTimeout public TimeSpan IdleTimeout
@@ -223,5 +224,22 @@ namespace AMWD.Protocols.Modbus.Serial
serialConnection.StopBits = value; serialConnection.StopBits = value;
} }
} }
/// <inheritdoc/>
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine($"Serial Client {PortName}");
sb.AppendLine($" {nameof(BaudRate)}: {(int)BaudRate}");
sb.AppendLine($" {nameof(DataBits)}: {DataBits}");
sb.AppendLine($" {nameof(StopBits)}: {(StopBits == StopBits.OnePointFive ? "1.5" : ((int)StopBits).ToString())}");
sb.AppendLine($" {nameof(Parity)}: {Parity.ToString().ToLower()}");
sb.AppendLine($" {nameof(Handshake)}: {Handshake.ToString().ToLower()}");
sb.AppendLine($" {nameof(RtsEnable)}: {RtsEnable.ToString().ToLower()}");
sb.AppendLine($" {nameof(DriverEnabledRS485)}: {DriverEnabledRS485.ToString().ToLower()}");
return sb.ToString();
}
} }
} }

View File

@@ -31,18 +31,24 @@ namespace AMWD.Protocols.Modbus.Serial
private readonly Task _processingTask; private readonly Task _processingTask;
private readonly AsyncQueue<RequestQueueItem> _requestQueue = new(); private readonly AsyncQueue<RequestQueueItem> _requestQueue = new();
// Only required to cover all logic branches on unit tests. private readonly bool _isLinux;
private bool _isUnitTest = false;
#endregion Fields #endregion Fields
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ModbusSerialConnection"/> class. /// Initializes a new instance of the <see cref="ModbusSerialConnection"/> class.
/// </summary> /// </summary>
public ModbusSerialConnection() public ModbusSerialConnection(string portName)
{ {
_isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
if (string.IsNullOrWhiteSpace(portName))
throw new ArgumentNullException(nameof(portName));
_serialPort = new SerialPortWrapper _serialPort = new SerialPortWrapper
{ {
PortName = portName,
BaudRate = (int)BaudRate.Baud19200, BaudRate = (int)BaudRate.Baud19200,
DataBits = 8, DataBits = 8,
Handshake = Handshake.None, Handshake = Handshake.None,
@@ -59,6 +65,9 @@ namespace AMWD.Protocols.Modbus.Serial
#region Properties #region Properties
/// <inheritdoc cref="SerialPort.GetPortNames" />
public static string[] AvailablePortNames => SerialPort.GetPortNames();
/// <inheritdoc/> /// <inheritdoc/>
public string Name => "Serial"; public string Name => "Serial";
@@ -68,20 +77,6 @@ namespace AMWD.Protocols.Modbus.Serial
/// <inheritdoc/> /// <inheritdoc/>
public virtual TimeSpan ConnectTimeout { get; set; } = TimeSpan.MaxValue; public virtual TimeSpan ConnectTimeout { get; set; } = TimeSpan.MaxValue;
/// <inheritdoc/>
public virtual TimeSpan ReadTimeout
{
get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout);
set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds;
}
/// <inheritdoc/>
public virtual TimeSpan WriteTimeout
{
get => TimeSpan.FromMilliseconds(_serialPort.WriteTimeout);
set => _serialPort.WriteTimeout = (int)value.TotalMilliseconds;
}
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the RS485 driver has to be enabled via software switch. /// Gets or sets a value indicating whether the RS485 driver has to be enabled via software switch.
/// </summary> /// </summary>
@@ -107,9 +102,7 @@ namespace AMWD.Protocols.Modbus.Serial
set => _serialPort.PortName = value; set => _serialPort.PortName = value;
} }
/// <summary> /// <inheritdoc cref="SerialPort.BaudRate" />
/// Gets or sets the serial baud rate.
/// </summary>
public virtual BaudRate BaudRate public virtual BaudRate BaudRate
{ {
get => (BaudRate)_serialPort.BaudRate; get => (BaudRate)_serialPort.BaudRate;
@@ -118,7 +111,11 @@ namespace AMWD.Protocols.Modbus.Serial
/// <inheritdoc cref="SerialPort.DataBits" /> /// <inheritdoc cref="SerialPort.DataBits" />
/// <remarks> /// <remarks>
/// Should be 7 for ASCII mode and 8 for RTU mode. /// From the Specs:
/// <br/>
/// On <see cref="AsciiProtocol"/> it can be 7 or 8.
/// <br/>
/// On <see cref="RtuProtocol"/> it has to be 8.
/// </remarks> /// </remarks>
public virtual int DataBits public virtual int DataBits
{ {
@@ -159,9 +156,9 @@ namespace AMWD.Protocols.Modbus.Serial
/// <remarks> /// <remarks>
/// From the Specs: /// From the Specs:
/// <br/> /// <br/>
/// Should be <see cref="StopBits.One"/> for <see cref="Parity.Even"/> or <see cref="Parity.Odd"/> and /// Should be <see cref="StopBits.One"/> for <see cref="Parity.Even"/> or <see cref="Parity.Odd"/>.
/// <br/> /// <br/>
/// should be <see cref="StopBits.Two"/> for <see cref="Parity.None"/>. /// Should be <see cref="StopBits.Two"/> for <see cref="Parity.None"/>.
/// </remarks> /// </remarks>
public virtual StopBits StopBits public virtual StopBits StopBits
{ {
@@ -169,6 +166,20 @@ namespace AMWD.Protocols.Modbus.Serial
set => _serialPort.StopBits = value; set => _serialPort.StopBits = value;
} }
/// <inheritdoc/>
public virtual TimeSpan ReadTimeout
{
get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout);
set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds;
}
/// <inheritdoc/>
public virtual TimeSpan WriteTimeout
{
get => TimeSpan.FromMilliseconds(_serialPort.WriteTimeout);
set => _serialPort.WriteTimeout = (int)value.TotalMilliseconds;
}
#endregion SerialPort Properties #endregion SerialPort Properties
#endregion Properties #endregion Properties
@@ -188,7 +199,6 @@ namespace AMWD.Protocols.Modbus.Serial
try try
{ {
_processingTask.Wait();
_processingTask.Dispose(); _processingTask.Dispose();
} }
catch catch
@@ -259,7 +269,7 @@ namespace AMWD.Protocols.Modbus.Serial
try try
{ {
// Get next request to process // Get next request to process
var item = await _requestQueue.DequeueAsync(cancellationToken); var item = await _requestQueue.DequeueAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
// Remove registration => already removed from queue // Remove registration => already removed from queue
item.CancellationTokenRegistration.Dispose(); item.CancellationTokenRegistration.Dispose();
@@ -267,13 +277,13 @@ namespace AMWD.Protocols.Modbus.Serial
// Build combined cancellation token // Build combined cancellation token
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
// Wait for exclusive access // Wait for exclusive access
await _portLock.WaitAsync(linkedCts.Token); await _portLock.WaitAsync(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
try try
{ {
// Ensure connection is up // Ensure connection is up
await AssertConnection(linkedCts.Token); await AssertConnection(linkedCts.Token);
await _serialPort.WriteAsync(item.Request, linkedCts.Token); await _serialPort.WriteAsync(item.Request, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
linkedCts.Token.ThrowIfCancellationRequested(); linkedCts.Token.ThrowIfCancellationRequested();
@@ -282,7 +292,7 @@ namespace AMWD.Protocols.Modbus.Serial
do do
{ {
int readCount = await _serialPort.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token); int readCount = await _serialPort.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
if (readCount < 1) if (readCount < 1)
throw new EndOfStreamException(); throw new EndOfStreamException();
@@ -313,7 +323,7 @@ namespace AMWD.Protocols.Modbus.Serial
_portLock.Release(); _portLock.Release();
_idleTimer.Change(IdleTimeout, Timeout.InfiniteTimeSpan); _idleTimer.Change(IdleTimeout, Timeout.InfiniteTimeSpan);
await Task.Delay(InterRequestDelay, cancellationToken); await Task.Delay(InterRequestDelay, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
} }
} }
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
@@ -344,7 +354,7 @@ namespace AMWD.Protocols.Modbus.Serial
_serialPort.Close(); _serialPort.Close();
_serialPort.ResetRS485DriverStateFlags(); _serialPort.ResetRS485DriverStateFlags();
if (DriverEnabledRS485 && (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || _isUnitTest)) if (DriverEnabledRS485 && _isLinux)
{ {
var flags = _serialPort.GetRS485DriverStateFlags(); var flags = _serialPort.GetRS485DriverStateFlags();
flags |= RS485Flags.Enabled; flags |= RS485Flags.Enabled;
@@ -352,7 +362,7 @@ namespace AMWD.Protocols.Modbus.Serial
_serialPort.ChangeRS485DriverStateFlags(flags); _serialPort.ChangeRS485DriverStateFlags(flags);
} }
using var connectTask = Task.Run(_serialPort.Open); using var connectTask = Task.Run(_serialPort.Open, cancellationToken);
if (await Task.WhenAny(connectTask, Task.Delay(ReadTimeout, cancellationToken)) == connectTask) if (await Task.WhenAny(connectTask, Task.Delay(ReadTimeout, cancellationToken)) == connectTask)
{ {
await connectTask; await connectTask;
@@ -370,7 +380,7 @@ namespace AMWD.Protocols.Modbus.Serial
try try
{ {
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken); await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
} }
catch catch
{ /* keep it quiet */ } { /* keep it quiet */ }

View File

@@ -44,10 +44,10 @@ using var client = new ModbusSerialClient(serialPort)
--- ---
Published under MIT License (see [**tl;dr**Legal]) Published under MIT License (see [choose a license])
[v1.1b3]: https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf [v1.1b3]: https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf
[v1.02]: https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf [v1.02]: https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf
[**tl;dr**Legal]: https://www.tldrlegal.com/license/mit-license [choose a license]: https://choosealicense.com/licenses/mit/

View File

@@ -20,6 +20,30 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
#endregion Fields #endregion Fields
#region Constructor
public SerialPortWrapper()
{
_serialPort.DataReceived += (sender, e) => DataReceived?.Invoke(this, e);
_serialPort.PinChanged += (sender, e) => PinChanged?.Invoke(this, e);
_serialPort.ErrorReceived += (sender, e) => ErrorReceived?.Invoke(this, e);
}
#endregion Constructor
#region Events
/// <inheritdoc cref="SerialPort.DataReceived"/>
public virtual event SerialDataReceivedEventHandler DataReceived;
/// <inheritdoc cref="SerialPort.PinChanged"/>
public virtual event SerialPinChangedEventHandler PinChanged;
/// <inheritdoc cref="SerialPort.ErrorReceived"/>
public virtual event SerialErrorReceivedEventHandler ErrorReceived;
#endregion Events
#region Properties #region Properties
/// <inheritdoc cref="SerialPort.Handshake"/> /// <inheritdoc cref="SerialPort.Handshake"/>
@@ -82,6 +106,10 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
set => _serialPort.Parity = value; set => _serialPort.Parity = value;
} }
/// <inheritdoc cref="SerialPort.BytesToWrite"/>
public virtual int BytesToWrite
=> _serialPort.BytesToWrite;
/// <inheritdoc cref="SerialPort.BaudRate"/> /// <inheritdoc cref="SerialPort.BaudRate"/>
public virtual int BaudRate public virtual int BaudRate
{ {
@@ -89,6 +117,10 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
set => _serialPort.BaudRate = value; set => _serialPort.BaudRate = value;
} }
/// <inheritdoc cref="SerialPort.BytesToRead"/>
public virtual int BytesToRead
=> _serialPort.BytesToRead;
#endregion Properties #endregion Properties
#region Methods #region Methods
@@ -101,6 +133,14 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
public virtual void Open() public virtual void Open()
=> _serialPort.Open(); => _serialPort.Open();
/// <inheritdoc cref="SerialPort.Read(byte[], int, int)"/>
public virtual int Read(byte[] buffer, int offset, int count)
=> _serialPort.Read(buffer, offset, count);
/// <inheritdoc cref="SerialPort.Write(byte[], int, int)"/>
public virtual void Write(byte[] buffer, int offset, int count)
=> _serialPort.Write(buffer, offset, count);
/// <inheritdoc cref="SerialPort.Dispose"/> /// <inheritdoc cref="SerialPort.Dispose"/>
public virtual void Dispose() public virtual void Dispose()
=> _serialPort.Dispose(); => _serialPort.Dispose();
@@ -117,7 +157,7 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
/// <remarks> /// <remarks>
/// There seems to be a bug with the async stream implementation on Windows. /// There seems to be a bug with the async stream implementation on Windows.
/// <br/> /// <br/>
/// See this StackOverflow answer: <see href="https://stackoverflow.com/a/54610437/11906695" /> /// See this StackOverflow answer: <see href="https://stackoverflow.com/a/54610437/11906695" />.
/// </remarks> /// </remarks>
/// <param name="buffer">The buffer to write the data into.</param> /// <param name="buffer">The buffer to write the data into.</param>
/// <param name="offset">The byte offset in buffer at which to begin writing data from the serial port.</param> /// <param name="offset">The byte offset in buffer at which to begin writing data from the serial port.</param>

View File

@@ -1,9 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
<LangVersion>12.0</LangVersion>
<PackageId>AMWD.Protocols.Modbus.Tcp</PackageId> <PackageId>AMWD.Protocols.Modbus.Tcp</PackageId>
<AssemblyName>amwd-modbus-tcp</AssemblyName> <AssemblyName>amwd-modbus-tcp</AssemblyName>
<RootNamespace>AMWD.Protocols.Modbus.Tcp</RootNamespace> <RootNamespace>AMWD.Protocols.Modbus.Tcp</RootNamespace>
@@ -14,17 +11,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="../AMWD.Protocols.Modbus.Common/InternalsVisibleTo.cs" Link="InternalsVisibleTo.cs" />
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" /> <Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" />
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ReaderWriterLockSlimExtensions.cs" Link="Extensions/ReaderWriterLockSlimExtensions.cs" /> <Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ReaderWriterLockSlimExtensions.cs" Link="Extensions/ReaderWriterLockSlimExtensions.cs" />
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/AsyncQueue.cs" Link="Utils/AsyncQueue.cs" /> <Compile Include="../AMWD.Protocols.Modbus.Common/Utils/AsyncQueue.cs" Link="Utils/AsyncQueue.cs" />
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/RequestQueueItem.cs" Link="Utils/RequestQueueItem.cs" /> <Compile Include="../AMWD.Protocols.Modbus.Common/Utils/RequestQueueItem.cs" Link="Utils/RequestQueueItem.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="/" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" /> <ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -0,0 +1,45 @@
using System.Threading;
using System.Threading.Tasks;
using AMWD.Protocols.Modbus.Tcp.Utils;
namespace System.IO
{
internal static class StreamExtensions
{
public static async Task<byte[]> ReadExpectedBytesAsync(this Stream stream, int expectedBytes, CancellationToken cancellationToken = default)
{
byte[] buffer = new byte[expectedBytes];
int offset = 0;
do
{
int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (count < 1)
throw new EndOfStreamException();
offset += count;
}
while (offset < expectedBytes && !cancellationToken.IsCancellationRequested);
cancellationToken.ThrowIfCancellationRequested();
return buffer;
}
public static async Task<byte[]> ReadExpectedBytesAsync(this NetworkStreamWrapper stream, int expectedBytes, CancellationToken cancellationToken = default)
{
byte[] buffer = new byte[expectedBytes];
int offset = 0;
do
{
int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (count < 1)
throw new EndOfStreamException();
offset += count;
}
while (offset < expectedBytes && !cancellationToken.IsCancellationRequested);
cancellationToken.ThrowIfCancellationRequested();
return buffer;
}
}
}

View File

@@ -0,0 +1,17 @@
using System.Threading.Tasks;
namespace AMWD.Protocols.Modbus.Tcp.Extensions
{
internal static class TaskExtensions
{
public static async void Forget(this Task task)
{
try
{
await task;
}
catch
{ /* keep it quiet */ }
}
}
}

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Text;
using AMWD.Protocols.Modbus.Common.Contracts; using AMWD.Protocols.Modbus.Common.Contracts;
using AMWD.Protocols.Modbus.Common.Protocols; using AMWD.Protocols.Modbus.Common.Protocols;
@@ -101,5 +102,16 @@ namespace AMWD.Protocols.Modbus.Tcp
tcpConnection.Port = value; tcpConnection.Port = value;
} }
} }
/// <inheritdoc/>
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine($"TCP Client {Hostname}");
sb.AppendLine($" {nameof(Port)}: {Port}");
return sb.ToString();
}
} }
} }

View File

@@ -34,8 +34,8 @@ namespace AMWD.Protocols.Modbus.Tcp
private readonly Task _processingTask; private readonly Task _processingTask;
private readonly AsyncQueue<RequestQueueItem> _requestQueue = new(); private readonly AsyncQueue<RequestQueueItem> _requestQueue = new();
private TimeSpan _readTimeout = TimeSpan.FromMilliseconds(1); private TimeSpan _readTimeout = TimeSpan.FromSeconds(1);
private TimeSpan _writeTimeout = TimeSpan.FromMilliseconds(1); private TimeSpan _writeTimeout = TimeSpan.FromSeconds(1);
#endregion Fields #endregion Fields
@@ -65,8 +65,12 @@ namespace AMWD.Protocols.Modbus.Tcp
get => _readTimeout; get => _readTimeout;
set set
{ {
#if NET8_0_OR_GREATER
ArgumentOutOfRangeException.ThrowIfLessThan(value, TimeSpan.Zero);
#else
if (value < TimeSpan.Zero) if (value < TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(value)); throw new ArgumentOutOfRangeException(nameof(value));
#endif
_readTimeout = value; _readTimeout = value;
@@ -81,8 +85,12 @@ namespace AMWD.Protocols.Modbus.Tcp
get => _writeTimeout; get => _writeTimeout;
set set
{ {
#if NET8_0_OR_GREATER
ArgumentOutOfRangeException.ThrowIfLessThan(value, TimeSpan.Zero);
#else
if (value < TimeSpan.Zero) if (value < TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(value)); throw new ArgumentOutOfRangeException(nameof(value));
#endif
_writeTimeout = value; _writeTimeout = value;
@@ -208,7 +216,7 @@ namespace AMWD.Protocols.Modbus.Tcp
try try
{ {
// Get next request to process // Get next request to process
var item = await _requestQueue.DequeueAsync(cancellationToken); var item = await _requestQueue.DequeueAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
// Remove registration => already removed from queue // Remove registration => already removed from queue
item.CancellationTokenRegistration.Dispose(); item.CancellationTokenRegistration.Dispose();
@@ -216,19 +224,19 @@ namespace AMWD.Protocols.Modbus.Tcp
// Build combined cancellation token // Build combined cancellation token
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
// Wait for exclusive access // Wait for exclusive access
await _clientLock.WaitAsync(linkedCts.Token); await _clientLock.WaitAsync(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
try try
{ {
// Ensure connection is up // Ensure connection is up
await AssertConnection(linkedCts.Token); await AssertConnection(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
var stream = _tcpClient.GetStream(); var stream = _tcpClient.GetStream();
await stream.FlushAsync(linkedCts.Token); await stream.FlushAsync(linkedCts.Token);
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
await stream.WriteAsync(item.Request, linkedCts.Token); await stream.WriteAsync(item.Request, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
#else #else
await stream.WriteAsync(item.Request, 0, item.Request.Length, linkedCts.Token); await stream.WriteAsync(item.Request, 0, item.Request.Length, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
#endif #endif
linkedCts.Token.ThrowIfCancellationRequested(); linkedCts.Token.ThrowIfCancellationRequested();
@@ -239,9 +247,9 @@ namespace AMWD.Protocols.Modbus.Tcp
do do
{ {
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
int readCount = await stream.ReadAsync(buffer, linkedCts.Token); int readCount = await stream.ReadAsync(buffer, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
#else #else
int readCount = await stream.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token); int readCount = await stream.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
#endif #endif
if (readCount < 1) if (readCount < 1)
throw new EndOfStreamException(); throw new EndOfStreamException();
@@ -332,7 +340,7 @@ namespace AMWD.Protocols.Modbus.Tcp
try try
{ {
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken); await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
} }
catch catch
{ /* keep it quiet */ } { /* keep it quiet */ }
@@ -376,10 +384,9 @@ namespace AMWD.Protocols.Modbus.Tcp
try try
{ {
return Dns.GetHostAddresses(hostname) return [.. Dns.GetHostAddresses(hostname)
.Where(a => a.AddressFamily == AddressFamily.InterNetwork || a.AddressFamily == AddressFamily.InterNetworkV6) .Where(a => a.AddressFamily == AddressFamily.InterNetwork || a.AddressFamily == AddressFamily.InterNetworkV6)
.OrderBy(a => a.AddressFamily) // prefer IPv4 .OrderBy(a => a.AddressFamily)]; // prefer IPv4
.ToArray();
} }
catch catch
{ {

View File

@@ -10,62 +10,38 @@ using System.Threading.Tasks;
using AMWD.Protocols.Modbus.Common; using AMWD.Protocols.Modbus.Common;
using AMWD.Protocols.Modbus.Common.Contracts; using AMWD.Protocols.Modbus.Common.Contracts;
using AMWD.Protocols.Modbus.Common.Protocols; using AMWD.Protocols.Modbus.Common.Protocols;
using AMWD.Protocols.Modbus.Tcp.Extensions;
using AMWD.Protocols.Modbus.Tcp.Utils;
namespace AMWD.Protocols.Modbus.Proxy namespace AMWD.Protocols.Modbus.Tcp
{ {
/// <summary> /// <summary>
/// Implements a Modbus TCP server proxying all requests to a Modbus client of choice. /// Implements a Modbus TCP server proxying all requests to a Modbus client of choice.
/// </summary> /// </summary>
public class ModbusTcpProxy : IDisposable /// <remarks>
/// Initializes a new instance of the <see cref="ModbusTcpProxy"/> class.
/// </remarks>
/// <param name="client">The <see cref="ModbusClientBase"/> used to request the remote device, that should be proxied.</param>
/// <param name="listenAddress">An <see cref="IPAddress"/> to listen on.</param>
public class ModbusTcpProxy(ModbusClientBase client, IPAddress listenAddress) : IModbusProxy
{ {
#region Fields #region Fields
private bool _isDisposed; private bool _isDisposed;
private TcpListener _listener; private TimeSpan _readWriteTimeout = TimeSpan.FromSeconds(100);
private readonly TcpListenerWrapper _tcpListener = new(listenAddress, 502);
private CancellationTokenSource _stopCts; private CancellationTokenSource _stopCts;
private Task _clientConnectTask = Task.CompletedTask; private Task _clientConnectTask = Task.CompletedTask;
private readonly SemaphoreSlim _clientListLock = new(1, 1); private readonly SemaphoreSlim _clientListLock = new(1, 1);
private readonly List<TcpClient> _clients = []; private readonly List<TcpClientWrapper> _clients = [];
private readonly List<Task> _clientTasks = [];
#endregion Fields #endregion Fields
#region Constructors #region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ModbusTcpProxy"/> class.
/// </summary>
/// <param name="client">The <see cref="ModbusClientBase"/> used to request the remote device, that should be proxied.</param>
/// <param name="listenAddress">An <see cref="IPAddress"/> to listen on (Default: <see cref="IPAddress.Loopback"/>).</param>
/// <param name="listenPort">A port to listen on (Default: 502).</param>
public ModbusTcpProxy(ModbusClientBase client, IPAddress listenAddress = null, int listenPort = 502)
{
Client = client ?? throw new ArgumentNullException(nameof(client));
ListenAddress = listenAddress ?? IPAddress.Loopback;
if (ushort.MinValue < listenPort || listenPort < ushort.MaxValue)
throw new ArgumentOutOfRangeException(nameof(listenPort));
try
{
#if NET8_0_OR_GREATER
using var testListener = new TcpListener(ListenAddress, listenPort);
#else
var testListener = new TcpListener(ListenAddress, listenPort);
#endif
testListener.Start(1);
ListenPort = (testListener.LocalEndpoint as IPEndPoint).Port;
testListener.Stop();
}
catch (Exception ex)
{
throw new ArgumentException($"{nameof(ListenPort)} ({listenPort}) is already in use.", ex);
}
}
#endregion Constructors #endregion Constructors
#region Properties #region Properties
@@ -73,27 +49,46 @@ namespace AMWD.Protocols.Modbus.Proxy
/// <summary> /// <summary>
/// Gets the Modbus client used to request the remote device, that should be proxied. /// Gets the Modbus client used to request the remote device, that should be proxied.
/// </summary> /// </summary>
public ModbusClientBase Client { get; } public ModbusClientBase Client { get; } = client ?? throw new ArgumentNullException(nameof(client));
/// <summary> /// <summary>
/// Gets the <see cref="IPAddress"/> to listen on. /// Gets the <see cref="IPAddress"/> to listen on.
/// </summary> /// </summary>
public IPAddress ListenAddress { get; } public IPAddress ListenAddress
{
get => _tcpListener.LocalIPEndPoint.Address;
set => _tcpListener.LocalIPEndPoint.Address = value;
}
/// <summary> /// <summary>
/// Get the port to listen on. /// Get the port to listen on.
/// </summary> /// </summary>
public int ListenPort { get; } public int ListenPort
{
get => _tcpListener.LocalIPEndPoint.Port;
set => _tcpListener.LocalIPEndPoint.Port = value;
}
/// <summary> /// <summary>
/// Gets a value indicating whether the server is running. /// Gets a value indicating whether the server is running.
/// </summary> /// </summary>
public bool IsRunning => _listener?.Server.IsBound ?? false; public bool IsRunning => _tcpListener.Socket.IsBound;
/// <summary> /// <summary>
/// Gets or sets the read/write timeout for the incoming connections (not the <see cref="Client"/>!). /// Gets or sets the read/write timeout for the incoming connections (not the <see cref="Client"/>!).
/// Default: 100 seconds.
/// </summary> /// </summary>
public TimeSpan ReadWriteTimeout { get; set; } public TimeSpan ReadWriteTimeout
{
get => _readWriteTimeout;
set
{
if (value != Timeout.InfiniteTimeSpan && value < TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(value));
_readWriteTimeout = value;
}
}
#endregion Properties #endregion Properties
@@ -108,20 +103,17 @@ namespace AMWD.Protocols.Modbus.Proxy
Assertions(); Assertions();
_stopCts?.Cancel(); _stopCts?.Cancel();
_tcpListener.Stop();
_listener?.Stop();
#if NET8_0_OR_GREATER
_listener?.Dispose();
#endif
_stopCts?.Dispose(); _stopCts?.Dispose();
_stopCts = new CancellationTokenSource(); _stopCts = new CancellationTokenSource();
_listener = new TcpListener(ListenAddress, ListenPort); // Only allowed to set, if the socket is in the InterNetworkV6 address family.
// See: https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.dualmode?view=netstandard-2.0#exceptions
if (ListenAddress.AddressFamily == AddressFamily.InterNetworkV6) if (ListenAddress.AddressFamily == AddressFamily.InterNetworkV6)
_listener.Server.DualMode = true; _tcpListener.Socket.DualMode = true;
_listener.Start(); _tcpListener.Start();
_clientConnectTask = WaitForClientAsync(_stopCts.Token); _clientConnectTask = WaitForClientAsync(_stopCts.Token);
return Task.CompletedTask; return Task.CompletedTask;
@@ -139,24 +131,12 @@ namespace AMWD.Protocols.Modbus.Proxy
private async Task StopAsyncInternal(CancellationToken cancellationToken = default) private async Task StopAsyncInternal(CancellationToken cancellationToken = default)
{ {
_stopCts.Cancel(); _stopCts?.Cancel();
_tcpListener.Stop();
_listener.Stop();
#if NET8_0_OR_GREATER
_listener.Dispose();
#endif
try
{
await Task.WhenAny(_clientConnectTask, Task.Delay(Timeout.Infinite, cancellationToken));
}
catch (OperationCanceledException)
{
// Terminated
}
try try
{ {
await Task.WhenAny(Task.WhenAll(_clientTasks), Task.Delay(Timeout.Infinite, cancellationToken)); await Task.WhenAny(_clientConnectTask, Task.Delay(Timeout.Infinite, cancellationToken)).ConfigureAwait(continueOnCapturedContext: false);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -178,8 +158,10 @@ namespace AMWD.Protocols.Modbus.Proxy
_clientListLock.Dispose(); _clientListLock.Dispose();
_clients.Clear(); _clients.Clear();
_tcpListener.Dispose();
_stopCts?.Dispose(); _stopCts?.Dispose();
GC.SuppressFinalize(this);
} }
private void Assertions() private void Assertions()
@@ -202,16 +184,13 @@ namespace AMWD.Protocols.Modbus.Proxy
{ {
try try
{ {
#if NET8_0_OR_GREATER var client = await _tcpListener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
var client = await _listener.AcceptTcpClientAsync(cancellationToken); await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
#else
var client = await _listener.AcceptTcpClientAsync();
#endif
await _clientListLock.WaitAsync(cancellationToken);
try try
{ {
_clients.Add(client); _clients.Add(client);
_clientTasks.Add(HandleClientAsync(client, cancellationToken)); // Can be ignored as it will terminate by itself on cancellation
HandleClientAsync(client, cancellationToken).Forget();
} }
finally finally
{ {
@@ -225,7 +204,7 @@ namespace AMWD.Protocols.Modbus.Proxy
} }
} }
private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken) private async Task HandleClientAsync(TcpClientWrapper client, CancellationToken cancellationToken)
{ {
try try
{ {
@@ -234,23 +213,32 @@ namespace AMWD.Protocols.Modbus.Proxy
{ {
var requestBytes = new List<byte>(); var requestBytes = new List<byte>();
// Waiting for next request
byte[] headerBytes = await stream.ReadExpectedBytesAsync(6, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
requestBytes.AddRange(headerBytes);
ushort length = headerBytes
.Skip(4).Take(2).ToArray()
.GetBigEndianUInt16();
// Waiting for the remaining required data
using (var cts = new CancellationTokenSource(ReadWriteTimeout)) using (var cts = new CancellationTokenSource(ReadWriteTimeout))
using (cancellationToken.Register(cts.Cancel)) using (cancellationToken.Register(cts.Cancel))
{ {
byte[] headerBytes = await stream.ReadExpectedBytesAsync(6, cts.Token); byte[] bodyBytes = await stream.ReadExpectedBytesAsync(length, cts.Token).ConfigureAwait(continueOnCapturedContext: false);
requestBytes.AddRange(headerBytes);
byte[] followingCountBytes = headerBytes.Skip(4).Take(2).ToArray();
followingCountBytes.SwapBigEndian();
int followingCount = BitConverter.ToUInt16(followingCountBytes, 0);
byte[] bodyBytes = await stream.ReadExpectedBytesAsync(followingCount, cts.Token);
requestBytes.AddRange(bodyBytes); requestBytes.AddRange(bodyBytes);
} }
byte[] responseBytes = await HandleRequestAsync([.. requestBytes], cancellationToken); byte[] responseBytes = await HandleRequestAsync([.. requestBytes], cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (responseBytes != null) if (responseBytes != null)
await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken); {
// Write response when available
using (var cts = new CancellationTokenSource(ReadWriteTimeout))
using (cancellationToken.Register(cts.Cancel))
{
await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cts.Token).ConfigureAwait(continueOnCapturedContext: false);
}
}
} }
} }
catch catch
@@ -259,7 +247,7 @@ namespace AMWD.Protocols.Modbus.Proxy
} }
finally finally
{ {
await _clientListLock.WaitAsync(cancellationToken); await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
try try
{ {
_clients.Remove(client); _clients.Remove(client);
@@ -309,14 +297,14 @@ namespace AMWD.Protocols.Modbus.Proxy
default: // unknown function default: // unknown function
{ {
byte[] responseBytes = new byte[9]; var responseBytes = new List<byte>();
Array.Copy(requestBytes, 0, responseBytes, 0, 8); responseBytes.AddRange(requestBytes.Take(8));
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
// Mark as error // Mark as error
responseBytes[7] |= 0x80; responseBytes[7] |= 0x80;
responseBytes[8] = (byte)ModbusErrorCode.IllegalFunction; return Task.FromResult(ReturnResponse(responseBytes));
return Task.FromResult(responseBytes);
} }
} }
} }
@@ -334,7 +322,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.AddRange(requestBytes.Take(8)); responseBytes.AddRange(requestBytes.Take(8));
try try
{ {
var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken); var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)]; byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)];
for (int i = 0; i < coils.Count; i++) for (int i = 0; i < coils.Count; i++)
@@ -357,7 +345,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
return [.. responseBytes]; return ReturnResponse(responseBytes);
} }
private async Task<byte[]> HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -373,7 +361,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.AddRange(requestBytes.Take(8)); responseBytes.AddRange(requestBytes.Take(8));
try try
{ {
var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken); var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)]; byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)];
for (int i = 0; i < discreteInputs.Count; i++) for (int i = 0; i < discreteInputs.Count; i++)
@@ -396,7 +384,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
return [.. responseBytes]; return ReturnResponse(responseBytes);
} }
private async Task<byte[]> HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -412,7 +400,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.AddRange(requestBytes.Take(8)); responseBytes.AddRange(requestBytes.Take(8));
try try
{ {
var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken); var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
byte[] values = new byte[holdingRegisters.Count * 2]; byte[] values = new byte[holdingRegisters.Count * 2];
for (int i = 0; i < holdingRegisters.Count; i++) for (int i = 0; i < holdingRegisters.Count; i++)
@@ -430,7 +418,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
return [.. responseBytes]; return ReturnResponse(responseBytes);
} }
private async Task<byte[]> HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -446,7 +434,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.AddRange(requestBytes.Take(8)); responseBytes.AddRange(requestBytes.Take(8));
try try
{ {
var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken); var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
byte[] values = new byte[count * 2]; byte[] values = new byte[count * 2];
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
@@ -464,7 +452,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
return [.. responseBytes]; return ReturnResponse(responseBytes);
} }
private async Task<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -481,7 +469,8 @@ namespace AMWD.Protocols.Modbus.Proxy
{ {
responseBytes[7] |= 0x80; responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
return [.. responseBytes];
return ReturnResponse(responseBytes);
} }
try try
@@ -493,7 +482,7 @@ namespace AMWD.Protocols.Modbus.Proxy
LowByte = requestBytes[11], LowByte = requestBytes[11],
}; };
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[6], coil, cancellationToken); bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[6], coil, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (isSuccess) if (isSuccess)
{ {
// Response is an echo of the request // Response is an echo of the request
@@ -511,7 +500,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
return [.. responseBytes]; return ReturnResponse(responseBytes);
} }
private async Task<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -533,7 +522,7 @@ namespace AMWD.Protocols.Modbus.Proxy
LowByte = requestBytes[11] LowByte = requestBytes[11]
}; };
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[6], register, cancellationToken); bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[6], register, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (isSuccess) if (isSuccess)
{ {
// Response is an echo of the request // Response is an echo of the request
@@ -551,7 +540,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
return [.. responseBytes]; return ReturnResponse(responseBytes);
} }
private async Task<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -570,7 +559,8 @@ namespace AMWD.Protocols.Modbus.Proxy
{ {
responseBytes[7] |= 0x80; responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
return [.. responseBytes];
return ReturnResponse(responseBytes);
} }
try try
@@ -592,7 +582,7 @@ namespace AMWD.Protocols.Modbus.Proxy
}); });
} }
bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[6], coils, cancellationToken); bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[6], coils, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (isSuccess) if (isSuccess)
{ {
// Response is an echo of the request // Response is an echo of the request
@@ -610,7 +600,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
return [.. responseBytes]; return ReturnResponse(responseBytes);
} }
private async Task<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -629,7 +619,8 @@ namespace AMWD.Protocols.Modbus.Proxy
{ {
responseBytes[7] |= 0x80; responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
return [.. responseBytes];
return ReturnResponse(responseBytes);
} }
try try
@@ -646,8 +637,9 @@ namespace AMWD.Protocols.Modbus.Proxy
HighByte = requestBytes[baseOffset + i * 2], HighByte = requestBytes[baseOffset + i * 2],
LowByte = requestBytes[baseOffset + i * 2 + 1] LowByte = requestBytes[baseOffset + i * 2 + 1]
}); });
}
bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[6], list, cancellationToken); bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[6], list, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (isSuccess) if (isSuccess)
{ {
// Response is an echo of the request // Response is an echo of the request
@@ -659,18 +651,20 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
} }
}
catch catch
{ {
responseBytes[7] |= 0x80; responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
return [.. responseBytes]; return ReturnResponse(responseBytes);
} }
private async Task<byte[]> HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken)
{ {
if (requestBytes.Length < 11)
return null;
var responseBytes = new List<byte>(); var responseBytes = new List<byte>();
responseBytes.AddRange(requestBytes.Take(8)); responseBytes.AddRange(requestBytes.Take(8));
@@ -678,7 +672,8 @@ namespace AMWD.Protocols.Modbus.Proxy
{ {
responseBytes[7] |= 0x80; responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction); responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
return [.. responseBytes];
return ReturnResponse(responseBytes);
} }
var firstObject = (ModbusDeviceIdentificationObject)requestBytes[10]; var firstObject = (ModbusDeviceIdentificationObject)requestBytes[10];
@@ -686,7 +681,8 @@ namespace AMWD.Protocols.Modbus.Proxy
{ {
responseBytes[7] |= 0x80; responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress); responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
return [.. responseBytes];
return ReturnResponse(responseBytes);
} }
var category = (ModbusDeviceIdentificationCategory)requestBytes[9]; var category = (ModbusDeviceIdentificationCategory)requestBytes[9];
@@ -694,12 +690,13 @@ namespace AMWD.Protocols.Modbus.Proxy
{ {
responseBytes[7] |= 0x80; responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
return [.. responseBytes];
return ReturnResponse(responseBytes);
} }
try try
{ {
var res = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken); var deviceInfo = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
var bodyBytes = new List<byte>(); var bodyBytes = new List<byte>();
@@ -708,31 +705,20 @@ namespace AMWD.Protocols.Modbus.Proxy
// Conformity // Conformity
bodyBytes.Add((byte)category); bodyBytes.Add((byte)category);
if (res.IsIndividualAccessAllowed) if (deviceInfo.IsIndividualAccessAllowed)
bodyBytes[2] |= 0x80; bodyBytes[2] |= 0x80;
// More, NextId, NumberOfObjects // More, NextId, NumberOfObjects
bodyBytes.AddRange(new byte[3]); bodyBytes.AddRange(new byte[3]);
int maxObjectId; int maxObjectId = category switch
switch (category)
{ {
case ModbusDeviceIdentificationCategory.Basic: ModbusDeviceIdentificationCategory.Basic => 0x02,
maxObjectId = 0x02; ModbusDeviceIdentificationCategory.Regular => 0x06,
break; ModbusDeviceIdentificationCategory.Extended => 0xFF,
// Individual
case ModbusDeviceIdentificationCategory.Regular: _ => requestBytes[10],
maxObjectId = 0x06; };
break;
case ModbusDeviceIdentificationCategory.Extended:
maxObjectId = 0xFF;
break;
default: // Individual
maxObjectId = requestBytes[10];
break;
}
byte numberOfObjects = 0; byte numberOfObjects = 0;
for (int i = requestBytes[10]; i <= maxObjectId; i++) for (int i = requestBytes[10]; i <= maxObjectId; i++)
@@ -741,7 +727,7 @@ namespace AMWD.Protocols.Modbus.Proxy
if (0x07 <= i && i <= 0x7F) if (0x07 <= i && i <= 0x7F)
continue; continue;
byte[] objBytes = GetDeviceObject((byte)i, res); byte[] objBytes = GetDeviceObject((byte)i, deviceInfo);
// We need to split the response if it would exceed the max ADU size // We need to split the response if it would exceed the max ADU size
if (responseBytes.Count + bodyBytes.Count + objBytes.Length > TcpProtocol.MAX_ADU_LENGTH) if (responseBytes.Count + bodyBytes.Count + objBytes.Length > TcpProtocol.MAX_ADU_LENGTH)
@@ -751,7 +737,8 @@ namespace AMWD.Protocols.Modbus.Proxy
bodyBytes[5] = numberOfObjects; bodyBytes[5] = numberOfObjects;
responseBytes.AddRange(bodyBytes); responseBytes.AddRange(bodyBytes);
return [.. responseBytes];
return ReturnResponse(responseBytes);
} }
bodyBytes.AddRange(objBytes); bodyBytes.AddRange(objBytes);
@@ -760,24 +747,26 @@ namespace AMWD.Protocols.Modbus.Proxy
bodyBytes[5] = numberOfObjects; bodyBytes[5] = numberOfObjects;
responseBytes.AddRange(bodyBytes); responseBytes.AddRange(bodyBytes);
return [.. responseBytes];
return ReturnResponse(responseBytes);
} }
catch catch
{ {
responseBytes[7] |= 0x80; responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
return [.. responseBytes];
return ReturnResponse(responseBytes);
} }
} }
private byte[] GetDeviceObject(byte objectId, DeviceIdentification deviceIdentification) private static byte[] GetDeviceObject(byte objectId, DeviceIdentification deviceIdentification)
{ {
var result = new List<byte> { objectId }; var result = new List<byte> { objectId };
switch ((ModbusDeviceIdentificationObject)objectId) switch ((ModbusDeviceIdentificationObject)objectId)
{ {
case ModbusDeviceIdentificationObject.VendorName: case ModbusDeviceIdentificationObject.VendorName:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -785,7 +774,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.ProductCode: case ModbusDeviceIdentificationObject.ProductCode:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -793,7 +782,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.MajorMinorRevision: case ModbusDeviceIdentificationObject.MajorMinorRevision:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -801,7 +790,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.VendorUrl: case ModbusDeviceIdentificationObject.VendorUrl:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -809,7 +798,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.ProductName: case ModbusDeviceIdentificationObject.ProductName:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -817,7 +806,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.ModelName: case ModbusDeviceIdentificationObject.ModelName:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -825,7 +814,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.UserApplicationName: case ModbusDeviceIdentificationObject.UserApplicationName:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -833,9 +822,8 @@ namespace AMWD.Protocols.Modbus.Proxy
default: default:
{ {
if (deviceIdentification.ExtendedObjects.ContainsKey(objectId)) if (deviceIdentification.ExtendedObjects.TryGetValue(objectId, out byte[] bytes))
{ {
byte[] bytes = deviceIdentification.ExtendedObjects[objectId];
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -850,6 +838,29 @@ namespace AMWD.Protocols.Modbus.Proxy
return [.. result]; return [.. result];
} }
private static byte[] ReturnResponse(List<byte> response)
{
ushort followingBytes = (ushort)(response.Count - 6);
var bytes = followingBytes.ToBigEndianBytes();
response[4] = bytes[0];
response[5] = bytes[1];
return [.. response];
}
#endregion Request Handling #endregion Request Handling
/// <inheritdoc/>
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine($"TCP Proxy");
sb.AppendLine($" {nameof(ListenAddress)}: {ListenAddress}");
sb.AppendLine($" {nameof(ListenPort)}: {ListenPort}");
sb.AppendLine($" {nameof(Client)}: {Client.GetType().Name}");
return sb.ToString();
}
} }
} }

View File

@@ -0,0 +1,95 @@
using System;
using System.Net;
using AMWD.Protocols.Modbus.Common;
using AMWD.Protocols.Modbus.Common.Events;
using AMWD.Protocols.Modbus.Common.Utils;
namespace AMWD.Protocols.Modbus.Tcp
{
/// <summary>
/// Implements a Modbus TCP server proxying all requests to a virtual Modbus client.
/// </summary>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public class ModbusTcpServer : ModbusTcpProxy
{
/// <summary>
/// Initializes a new instance of the <see cref="ModbusTcpServer"/> class.
/// </summary>
/// <param name="listenAddress">The <see cref="IPAddress"/> to listen on.</param>
public ModbusTcpServer(IPAddress listenAddress)
: base(new VirtualModbusClient(), listenAddress)
{
TypedClient.CoilWritten += (sender, e) => CoilWritten?.Invoke(this, e);
TypedClient.RegisterWritten += (sender, e) => RegisterWritten?.Invoke(this, e);
}
#region Events
/// <summary>
/// Indicates that a <see cref="Coil"/>-value received through a remote client has been written.
/// </summary>
public event EventHandler<CoilWrittenEventArgs> CoilWritten;
/// <summary>
/// Indicates that a <see cref="HoldingRegister"/>-value received from a remote client has been written.
/// </summary>
public event EventHandler<RegisterWrittenEventArgs> RegisterWritten;
#endregion Events
#region Properties
internal VirtualModbusClient TypedClient
=> Client as VirtualModbusClient;
#endregion Properties
#region Device Handling
/// <inheritdoc cref="VirtualModbusClient.AddDevice(byte)"/>
public bool AddDevice(byte unitId)
=> TypedClient.AddDevice(unitId);
/// <inheritdoc cref="VirtualModbusClient.RemoveDevice(byte)"/>
public bool RemoveDevice(byte unitId)
=> TypedClient.RemoveDevice(unitId);
#endregion Device Handling
#region Entity Handling
/// <inheritdoc cref="VirtualModbusClient.GetCoil(byte, ushort)"/>
public Coil GetCoil(byte unitId, ushort address)
=> TypedClient.GetCoil(unitId, address);
/// <inheritdoc cref="VirtualModbusClient.SetCoil(byte, Coil)"/>
public void SetCoil(byte unitId, Coil coil)
=> TypedClient.SetCoil(unitId, coil);
/// <inheritdoc cref="VirtualModbusClient.GetDiscreteInput(byte, ushort)"/>
public DiscreteInput GetDiscreteInput(byte unitId, ushort address)
=> TypedClient.GetDiscreteInput(unitId, address);
/// <inheritdoc cref="VirtualModbusClient.SetDiscreteInput(byte, DiscreteInput)"/>
public void SetDiscreteInput(byte unitId, DiscreteInput discreteInput)
=> TypedClient.SetDiscreteInput(unitId, discreteInput);
/// <inheritdoc cref="VirtualModbusClient.GetHoldingRegister(byte, ushort)"/>
public HoldingRegister GetHoldingRegister(byte unitId, ushort address)
=> TypedClient.GetHoldingRegister(unitId, address);
/// <inheritdoc cref="VirtualModbusClient.SetHoldingRegister(byte, HoldingRegister)"/>
public void SetHoldingRegister(byte unitId, HoldingRegister holdingRegister)
=> TypedClient.SetHoldingRegister(unitId, holdingRegister);
/// <inheritdoc cref="VirtualModbusClient.GetInputRegister(byte, ushort)"/>
public InputRegister GetInputRegister(byte unitId, ushort address)
=> TypedClient.GetInputRegister(unitId, address);
/// <inheritdoc cref="VirtualModbusClient.SetInputRegister(byte, InputRegister)"/>
public void SetInputRegister(byte unitId, InputRegister inputRegister)
=> TypedClient.SetInputRegister(unitId, inputRegister);
#endregion Entity Handling
}
}

View File

@@ -23,14 +23,14 @@ float voltage = registers.GetSingle();
Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V"); Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V");
``` ```
If you want to use the `RTU over TCP` protocol instead, you can do this on initialization: If you have a device speaking `RTU` connected over `TCP`, you can use it as followed:
```csharp ```csharp
// [...] // [...]
using var client = new ModbusTcpClient(host, port) using var client = new ModbusTcpClient(host, port)
{ {
Protocol = new RtuOverTcpProtocol(); Protocol = new RtuProtocol()
}; };
// [...] // [...]
@@ -44,10 +44,10 @@ using var client = new ModbusTcpClient(host, port)
--- ---
Published under MIT License (see [**tl;dr**Legal]) Published under MIT License (see [choose a license])
[v1.1b3]: https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf [v1.1b3]: https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf
[v1.0b]: https://modbus.org/docs/Modbus_Messaging_Implementation_Guide_V1_0b.pdf [v1.0b]: https://modbus.org/docs/Modbus_Messaging_Implementation_Guide_V1_0b.pdf
[**tl;dr**Legal]: https://www.tldrlegal.com/license/mit-license [choose a license]: https://choosealicense.com/licenses/mit/

View File

@@ -0,0 +1,30 @@
using System.Net;
using System.Net.Sockets;
namespace AMWD.Protocols.Modbus.Tcp.Utils
{
/// <inheritdoc cref="IPEndPoint" />
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal class IPEndPointWrapper(EndPoint endPoint)
{
private readonly IPEndPoint _ipEndPoint = (IPEndPoint)endPoint;
#region Properties
/// <inheritdoc cref="IPEndPoint.Address"/>
public virtual IPAddress Address
{
get => _ipEndPoint.Address;
set => _ipEndPoint.Address = value;
}
/// <inheritdoc cref="IPEndPoint.Port"/>
public virtual int Port
{
get => _ipEndPoint.Port;
set => _ipEndPoint.Port = value;
}
#endregion Properties
}
}

View File

@@ -8,18 +8,9 @@ namespace AMWD.Protocols.Modbus.Tcp.Utils
{ {
/// <inheritdoc cref="NetworkStream" /> /// <inheritdoc cref="NetworkStream" />
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal class NetworkStreamWrapper : IDisposable internal class NetworkStreamWrapper(NetworkStream stream) : IDisposable
{ {
private readonly NetworkStream _stream; private readonly NetworkStream _stream = stream;
[Obsolete("Constructor only for mocking on UnitTests!", error: true)]
public NetworkStreamWrapper()
{ }
public NetworkStreamWrapper(NetworkStream stream)
{
_stream = stream;
}
/// <inheritdoc cref="NetworkStream.Dispose" /> /// <inheritdoc cref="NetworkStream.Dispose" />
public virtual void Dispose() public virtual void Dispose()

View File

@@ -0,0 +1,26 @@
using System;
using System.Net.Sockets;
namespace AMWD.Protocols.Modbus.Tcp.Utils
{
/// <inheritdoc cref="Socket" />
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal class SocketWrapper(Socket socket) : IDisposable
{
private readonly Socket _socket = socket;
/// <inheritdoc cref="Socket.DualMode" />
public virtual bool DualMode
{
get => _socket.DualMode;
set => _socket.DualMode = value;
}
/// <inheritdoc cref="Socket.IsBound" />
public virtual bool IsBound
=> _socket.IsBound;
public virtual void Dispose()
=> _socket.Dispose();
}
}

View File

@@ -3,19 +3,30 @@ using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Transactions;
namespace AMWD.Protocols.Modbus.Tcp.Utils namespace AMWD.Protocols.Modbus.Tcp.Utils
{ {
/// <inheritdoc cref="TcpClient" /> /// <inheritdoc cref="TcpClient" />
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal class TcpClientWrapper(AddressFamily addressFamily) : IDisposable internal class TcpClientWrapper : IDisposable
{ {
#region Fields #region Fields
private readonly TcpClient _client = new(addressFamily); private readonly TcpClient _client;
#endregion Fields #endregion Fields
public TcpClientWrapper(AddressFamily addressFamily)
{
_client = new TcpClient(addressFamily);
}
public TcpClientWrapper(TcpClient client)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
}
#region Properties #region Properties
/// <inheritdoc cref="TcpClient.Connected" /> /// <inheritdoc cref="TcpClient.Connected" />

View File

@@ -3,6 +3,9 @@ using System.Net.Sockets;
namespace AMWD.Protocols.Modbus.Tcp.Utils namespace AMWD.Protocols.Modbus.Tcp.Utils
{ {
/// <summary>
/// Factory for creating <see cref="TcpClientWrapper"/> instances.
/// </summary>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal class TcpClientWrapperFactory internal class TcpClientWrapperFactory
{ {

View File

@@ -0,0 +1,87 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace AMWD.Protocols.Modbus.Tcp.Utils
{
/// <inheritdoc cref="TcpListener" />
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal class TcpListenerWrapper(IPAddress localaddr, int port) : IDisposable
{
#region Fields
private readonly TcpListener _tcpListener = new(localaddr, port);
#endregion Fields
#region Constructor
#endregion Constructor
#region Properties
/// <inheritdoc cref="TcpListener.LocalEndpoint"/>
public virtual IPEndPointWrapper LocalIPEndPoint
=> new(_tcpListener.LocalEndpoint);
public virtual SocketWrapper Socket
=> new(_tcpListener.Server);
#endregion Properties
#region Methods
/// <summary>
/// Accepts a pending connection request as a cancellable asynchronous operation.
/// </summary>
/// <remarks>
/// <para>
/// This operation will not block. The returned <see cref="Task{TResult}"/> object will complete after the TCP connection has been accepted.
/// </para>
/// <para>
/// Use the <see cref="TcpClientWrapper.GetStream"/> method to obtain the underlying <see cref="NetworkStreamWrapper"/> of the returned <see cref="TcpClientWrapper"/> in the <see cref="Task{TResult}"/>.
/// The <see cref="NetworkStreamWrapper"/> will provide you with methods for sending and receiving with the remote host.
/// When you are through with the <see cref="TcpClientWrapper"/>, be sure to call its <see cref="TcpClientWrapper.Close"/> method.
/// </para>
/// </remarks>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation</param>
/// <returns>
/// The task object representing the asynchronous operation.
/// The <see cref="Task{TResult}.Result"/> property on the task object returns a <see cref="TcpClientWrapper"/> used to send and receive data.
/// </returns>
/// <exception cref="InvalidOperationException">The listener has not been started with a call to <see cref="Start"/>.</exception>
/// <exception cref="SocketException">
/// Use the <see cref="SocketException.ErrorCode"/> property to obtain the specific error code.
/// When you have obtained this code, you can refer to the
/// <see href="https://learn.microsoft.com/en-us/windows/desktop/winsock/windows-sockets-error-codes-2">Windows Sockets version 2 API error code</see>
/// documentation for a detailed description of the error.
/// </exception>
/// <exception cref="OperationCanceledException">The cancellation token was canceled. This exception is stored into the returned task.</exception>
public virtual async Task<TcpClientWrapper> AcceptTcpClientAsync(CancellationToken cancellationToken = default)
{
#if NET8_0_OR_GREATER
var tcpClient = await _tcpListener.AcceptTcpClientAsync(cancellationToken);
#else
var tcpClient = await _tcpListener.AcceptTcpClientAsync();
#endif
return new TcpClientWrapper(tcpClient);
}
public virtual void Start()
=> _tcpListener.Start();
public virtual void Stop()
=> _tcpListener.Stop();
public virtual void Dispose()
{
#if NET8_0_OR_GREATER
_tcpListener.Dispose();
#endif
}
#endregion Methods
}
}

51
src/Directory.Build.props Normal file
View File

@@ -0,0 +1,51 @@
<Project>
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
<NrtContinuousIntegrationBuild>$(ContinuousIntegrationBuild)</NrtContinuousIntegrationBuild>
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/AM-WD/AMWD.Protocols.Modbus.git</RepositoryUrl>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PackageIcon>package-icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
<PackageProjectUrl>https://modbus.org/tech.php</PackageProjectUrl>
<AssemblyOriginatorKeyFile>../../AMWD.Protocols.Modbus.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<PropertyGroup Condition="'$(CI)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>
<ItemGroup Condition="'$(CI)' == 'true'">
<SourceLinkGiteaHost Include="$(CI_SERVER_HOST)" />
<PackageReference Include="Microsoft.SourceLink.Gitea" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<None Include="../../package-icon.png" Pack="true" PackagePath="/" />
<None Include="../../LICENSE.txt" Pack="true" PackagePath="/" />
<None Include="README.md" Pack="true" PackagePath="/" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="AMWD.Protocols.Modbus.Tests" PublicKey="$(PublicKey)" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" PublicKey="$(MoqPublicKey)" />
</ItemGroup>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)\..'))" />
</Project>

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup />
<ItemGroup>
<Compile Include="../MSTestSettings.cs" Link="MSTestSettings.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj" />
<ProjectReference Include="..\..\src\AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,14 +1,15 @@
using System.Collections.Generic; using System.Text;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Moq; using AMWD.Protocols.Modbus.Common.Contracts;
namespace AMWD.Protocols.Modbus.Tests.Common.Contracts namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
{ {
[TestClass] [TestClass]
public class ModbusClientBaseTest public class ModbusClientBaseTest
{ {
public TestContext TestContext { get; set; }
// Consts // Consts
private const byte UNIT_ID = 42; private const byte UNIT_ID = 42;
private const ushort START_ADDRESS = 123; private const ushort START_ADDRESS = 123;
@@ -105,19 +106,16 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowExceptionOnNullConnection() public void ShouldThrowExceptionOnNullConnection()
{ {
// Arrange // Arrange
IModbusConnection connection = null; IModbusConnection connection = null;
// Act // Act + Assert
new ModbusClientBaseWrapper(connection); Assert.ThrowsExactly<ArgumentNullException>(() => new ModbusClientBaseWrapper(connection));
// Assert - ArgumentNullException
} }
[DataTestMethod] [TestMethod]
[DataRow(true)] [DataRow(true)]
[DataRow(false)] [DataRow(false)]
public void ShouldAlsoDisposeConnection(bool disposeConnection) public void ShouldAlsoDisposeConnection(bool disposeConnection)
@@ -155,31 +153,25 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ObjectDisposedException))]
public async Task ShouldAssertDisposed() public async Task ShouldAssertDisposed()
{ {
// Arrange // Arrange
var client = GetClient(); var client = GetClient();
client.Dispose(); client.Dispose();
// Act // Act + Assert
await client.ReadCoilsAsync(UNIT_ID, START_ADDRESS, READ_COUNT); await Assert.ThrowsExactlyAsync<ObjectDisposedException>(() => client.ReadCoilsAsync(UNIT_ID, START_ADDRESS, READ_COUNT, TestContext.CancellationToken));
// Assert - ObjectDisposedException
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public async Task ShouldAssertProtocolSet() public async Task ShouldAssertProtocolSet()
{ {
// Arrange // Arrange
var client = GetClient(); var client = GetClient();
client.Protocol = null; client.Protocol = null;
// Act // Act + Assert
await client.ReadCoilsAsync(UNIT_ID, START_ADDRESS, READ_COUNT); await Assert.ThrowsExactlyAsync<ArgumentNullException>(() => client.ReadCoilsAsync(UNIT_ID, START_ADDRESS, READ_COUNT, TestContext.CancellationToken));
// Assert - ArgumentNullException
} }
#endregion Common/Connection/Assertions #endregion Common/Connection/Assertions
@@ -194,11 +186,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
var client = GetClient(); var client = GetClient();
// Act // Act
var result = await client.ReadCoilsAsync(UNIT_ID, START_ADDRESS, READ_COUNT); var result = await client.ReadCoilsAsync(UNIT_ID, START_ADDRESS, READ_COUNT, TestContext.CancellationToken);
// Assert // Assert
Assert.IsNotNull(result); Assert.IsNotNull(result);
Assert.AreEqual(READ_COUNT, result.Count); Assert.HasCount(READ_COUNT, result);
for (int i = 0; i < READ_COUNT; i++) for (int i = 0; i < READ_COUNT; i++)
{ {
@@ -223,11 +215,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
var client = GetClient(); var client = GetClient();
// Act // Act
var result = await client.ReadDiscreteInputsAsync(UNIT_ID, START_ADDRESS, READ_COUNT); var result = await client.ReadDiscreteInputsAsync(UNIT_ID, START_ADDRESS, READ_COUNT, TestContext.CancellationToken);
// Assert // Assert
Assert.IsNotNull(result); Assert.IsNotNull(result);
Assert.AreEqual(READ_COUNT, result.Count); Assert.HasCount(READ_COUNT, result);
for (int i = 0; i < READ_COUNT; i++) for (int i = 0; i < READ_COUNT; i++)
{ {
@@ -251,11 +243,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
var client = GetClient(); var client = GetClient();
// Act // Act
var result = await client.ReadHoldingRegistersAsync(UNIT_ID, START_ADDRESS, READ_COUNT); var result = await client.ReadHoldingRegistersAsync(UNIT_ID, START_ADDRESS, READ_COUNT, TestContext.CancellationToken);
// Assert // Assert
Assert.IsNotNull(result); Assert.IsNotNull(result);
Assert.AreEqual(READ_COUNT, result.Count); Assert.HasCount(READ_COUNT, result);
for (int i = 0; i < READ_COUNT; i++) for (int i = 0; i < READ_COUNT; i++)
{ {
@@ -279,11 +271,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
var client = GetClient(); var client = GetClient();
// Act // Act
var result = await client.ReadInputRegistersAsync(UNIT_ID, START_ADDRESS, READ_COUNT); var result = await client.ReadInputRegistersAsync(UNIT_ID, START_ADDRESS, READ_COUNT, TestContext.CancellationToken);
// Assert // Assert
Assert.IsNotNull(result); Assert.IsNotNull(result);
Assert.AreEqual(READ_COUNT, result.Count); Assert.HasCount(READ_COUNT, result);
for (int i = 0; i < READ_COUNT; i++) for (int i = 0; i < READ_COUNT; i++)
{ {
@@ -307,7 +299,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
var client = GetClient(); var client = GetClient();
// Act // Act
var result = await client.ReadDeviceIdentificationAsync(UNIT_ID, ModbusDeviceIdentificationCategory.Basic, ModbusDeviceIdentificationObject.VendorName); var result = await client.ReadDeviceIdentificationAsync(UNIT_ID, ModbusDeviceIdentificationCategory.Basic, ModbusDeviceIdentificationObject.VendorName, TestContext.CancellationToken);
// Assert // Assert
Assert.IsNotNull(result); Assert.IsNotNull(result);
@@ -320,7 +312,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
Assert.AreEqual("UnitTests", result.ModelName); Assert.AreEqual("UnitTests", result.ModelName);
Assert.AreEqual("Modbus Client Base Unit Test", result.UserApplicationName); Assert.AreEqual("Modbus Client Base Unit Test", result.UserApplicationName);
Assert.AreEqual(0, result.ExtendedObjects.Count); Assert.IsEmpty(result.ExtendedObjects);
_connection.Verify(c => c.InvokeAsync(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<Func<IReadOnlyList<byte>, bool>>(), It.IsAny<CancellationToken>()), Times.Once); _connection.Verify(c => c.InvokeAsync(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<Func<IReadOnlyList<byte>, bool>>(), It.IsAny<CancellationToken>()), Times.Once);
_connection.VerifyNoOtherCalls(); _connection.VerifyNoOtherCalls();
@@ -350,7 +342,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
var client = GetClient(); var client = GetClient();
// Act // Act
var result = await client.ReadDeviceIdentificationAsync(UNIT_ID, ModbusDeviceIdentificationCategory.Extended, ModbusDeviceIdentificationObject.VendorName); var result = await client.ReadDeviceIdentificationAsync(UNIT_ID, ModbusDeviceIdentificationCategory.Extended, ModbusDeviceIdentificationObject.VendorName, TestContext.CancellationToken);
// Assert // Assert
Assert.IsNotNull(result); Assert.IsNotNull(result);
@@ -363,7 +355,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
Assert.AreEqual("UnitTests", result.ModelName); Assert.AreEqual("UnitTests", result.ModelName);
Assert.AreEqual("Modbus Client Base Unit Test", result.UserApplicationName); Assert.AreEqual("Modbus Client Base Unit Test", result.UserApplicationName);
Assert.AreEqual(1, result.ExtendedObjects.Count); Assert.HasCount(1, result.ExtendedObjects);
Assert.AreEqual(0x07, result.ExtendedObjects.First().Key); Assert.AreEqual(0x07, result.ExtendedObjects.First().Key);
CollectionAssert.AreEqual(new byte[] { 0x01, 0x02, 0x03 }, result.ExtendedObjects.First().Value); CollectionAssert.AreEqual(new byte[] { 0x01, 0x02, 0x03 }, result.ExtendedObjects.First().Value);
@@ -393,7 +385,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
var client = GetClient(); var client = GetClient();
// Act // Act
bool result = await client.WriteSingleCoilAsync(UNIT_ID, coil); bool result = await client.WriteSingleCoilAsync(UNIT_ID, coil, TestContext.CancellationToken);
// Assert // Assert
Assert.IsTrue(result); Assert.IsTrue(result);
@@ -419,7 +411,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
var client = GetClient(); var client = GetClient();
// Act // Act
bool result = await client.WriteSingleCoilAsync(UNIT_ID, coil); bool result = await client.WriteSingleCoilAsync(UNIT_ID, coil, TestContext.CancellationToken);
// Assert // Assert
Assert.IsFalse(result); Assert.IsFalse(result);
@@ -445,7 +437,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
var client = GetClient(); var client = GetClient();
// Act // Act
bool result = await client.WriteSingleCoilAsync(UNIT_ID, coil); bool result = await client.WriteSingleCoilAsync(UNIT_ID, coil, TestContext.CancellationToken);
// Assert // Assert
Assert.IsFalse(result); Assert.IsFalse(result);
@@ -471,7 +463,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
var client = GetClient(); var client = GetClient();
// Act // Act
bool result = await client.WriteSingleHoldingRegisterAsync(UNIT_ID, register); bool result = await client.WriteSingleHoldingRegisterAsync(UNIT_ID, register, TestContext.CancellationToken);
// Assert // Assert
Assert.IsTrue(result); Assert.IsTrue(result);
@@ -497,7 +489,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
var client = GetClient(); var client = GetClient();
// Act // Act
bool result = await client.WriteSingleHoldingRegisterAsync(UNIT_ID, register); bool result = await client.WriteSingleHoldingRegisterAsync(UNIT_ID, register, TestContext.CancellationToken);
// Assert // Assert
Assert.IsFalse(result); Assert.IsFalse(result);
@@ -523,7 +515,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
var client = GetClient(); var client = GetClient();
// Act // Act
bool result = await client.WriteSingleHoldingRegisterAsync(UNIT_ID, register); bool result = await client.WriteSingleHoldingRegisterAsync(UNIT_ID, register, TestContext.CancellationToken);
// Assert // Assert
Assert.IsFalse(result); Assert.IsFalse(result);
@@ -553,7 +545,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
var client = GetClient(); var client = GetClient();
// Act // Act
bool result = await client.WriteMultipleCoilsAsync(UNIT_ID, coils); bool result = await client.WriteMultipleCoilsAsync(UNIT_ID, coils, TestContext.CancellationToken);
// Assert // Assert
Assert.IsTrue(result); Assert.IsTrue(result);
@@ -584,7 +576,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
var client = GetClient(); var client = GetClient();
// Act // Act
bool result = await client.WriteMultipleCoilsAsync(UNIT_ID, coils); bool result = await client.WriteMultipleCoilsAsync(UNIT_ID, coils, TestContext.CancellationToken);
// Assert // Assert
Assert.IsFalse(result); Assert.IsFalse(result);
@@ -615,7 +607,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
var client = GetClient(); var client = GetClient();
// Act // Act
bool result = await client.WriteMultipleCoilsAsync(UNIT_ID, coils); bool result = await client.WriteMultipleCoilsAsync(UNIT_ID, coils, TestContext.CancellationToken);
// Assert // Assert
Assert.IsFalse(result); Assert.IsFalse(result);
@@ -645,7 +637,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
var client = GetClient(); var client = GetClient();
// Act // Act
bool result = await client.WriteMultipleHoldingRegistersAsync(UNIT_ID, registers); bool result = await client.WriteMultipleHoldingRegistersAsync(UNIT_ID, registers, TestContext.CancellationToken);
// Assert // Assert
Assert.IsTrue(result); Assert.IsTrue(result);
@@ -676,7 +668,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
var client = GetClient(); var client = GetClient();
// Act // Act
bool result = await client.WriteMultipleHoldingRegistersAsync(UNIT_ID, registers); bool result = await client.WriteMultipleHoldingRegistersAsync(UNIT_ID, registers, TestContext.CancellationToken);
// Assert // Assert
Assert.IsFalse(result); Assert.IsFalse(result);
@@ -707,7 +699,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
var client = GetClient(); var client = GetClient();
// Act // Act
bool result = await client.WriteMultipleHoldingRegistersAsync(UNIT_ID, registers); bool result = await client.WriteMultipleHoldingRegistersAsync(UNIT_ID, registers, TestContext.CancellationToken);
// Assert // Assert
Assert.IsFalse(result); Assert.IsFalse(result);

View File

@@ -41,20 +41,16 @@
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowNullOnGetSingle() public void ShouldThrowNullOnGetSingle()
{ {
// Arrange // Arrange
HoldingRegister[] registers = null; HoldingRegister[] registers = null;
// Act // Act + Assert
registers.GetSingle(0); Assert.ThrowsExactly<ArgumentNullException>(() => registers.GetSingle(0));
// Assert - ArgumentNullException
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentOnGetSingleForLength() public void ShouldThrowArgumentOnGetSingleForLength()
{ {
// Arrange // Arrange
@@ -63,16 +59,13 @@
new() { Address = 101, HighByte = 0x01, LowByte = 0x02 } new() { Address = 101, HighByte = 0x01, LowByte = 0x02 }
}; };
// Act // Act + Assert
registers.GetSingle(0); Assert.ThrowsExactly<ArgumentException>(() => registers.GetSingle(0));
// Assert - ArgumentException
} }
[DataTestMethod] [TestMethod]
[DataRow(1)] [DataRow(1)]
[DataRow(-1)] [DataRow(-1)]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ShouldThrowArgumentOutOfRangeOnGetSingle(int startIndex) public void ShouldThrowArgumentOutOfRangeOnGetSingle(int startIndex)
{ {
// Arrange // Arrange
@@ -82,14 +75,11 @@
new() { Address = 100, HighByte = 0x03, LowByte = 0x04 } new() { Address = 100, HighByte = 0x03, LowByte = 0x04 }
}; };
// Act // Act + Assert
registers.GetSingle(startIndex); Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => registers.GetSingle(startIndex));
// Assert - ArgumentOutOfRangeException
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentOnGetSingleForType() public void ShouldThrowArgumentOnGetSingleForType()
{ {
// Arrange // Arrange
@@ -99,10 +89,8 @@
new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 } new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 }
}; };
// Act // Act + Assert
registers.GetSingle(0); Assert.ThrowsExactly<ArgumentException>(() => registers.GetSingle(0));
// Assert - ArgumentException
} }
[TestMethod] [TestMethod]
@@ -145,20 +133,16 @@
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowNullOnGetDouble() public void ShouldThrowNullOnGetDouble()
{ {
// Arrange // Arrange
HoldingRegister[] registers = null; HoldingRegister[] registers = null;
// Act // Act + Assert
registers.GetDouble(0); Assert.ThrowsExactly<ArgumentNullException>(() => registers.GetDouble(0));
// Assert - ArgumentNullException
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentOnGetDoubleForLength() public void ShouldThrowArgumentOnGetDoubleForLength()
{ {
// Arrange // Arrange
@@ -169,16 +153,13 @@
new() { Address = 102, HighByte = 0x7A, LowByte = 0xE1 } new() { Address = 102, HighByte = 0x7A, LowByte = 0xE1 }
}; };
// Act // Act + Assert
registers.GetDouble(0); Assert.ThrowsExactly<ArgumentException>(() => registers.GetDouble(0));
// Assert - ArgumentException
} }
[DataTestMethod] [TestMethod]
[DataRow(1)] [DataRow(1)]
[DataRow(-1)] [DataRow(-1)]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ShouldThrowArgumentOutOfRangeOnGetDouble(int startIndex) public void ShouldThrowArgumentOutOfRangeOnGetDouble(int startIndex)
{ {
// Arrange // Arrange
@@ -190,14 +171,11 @@
new() { Address = 103, HighByte = 0x47, LowByte = 0xAE } new() { Address = 103, HighByte = 0x47, LowByte = 0xAE }
}; };
// Act // Act + Assert
registers.GetDouble(startIndex); Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => registers.GetDouble(startIndex));
// Assert - ArgumentOutOfRangeException
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentOnGetDoubleForType() public void ShouldThrowArgumentOnGetDoubleForType()
{ {
// Arrange // Arrange
@@ -209,10 +187,8 @@
new InputRegister { Address = 103, HighByte = 0x47, LowByte = 0xAE } new InputRegister { Address = 103, HighByte = 0x47, LowByte = 0xAE }
}; };
// Act // Act + Assert
registers.GetDouble(0); Assert.ThrowsExactly<ArgumentException>(() => registers.GetDouble(0));
// Assert - ArgumentException
} }
#endregion Modbus to value #endregion Modbus to value
@@ -230,7 +206,7 @@
// Assert // Assert
Assert.IsNotNull(registers); Assert.IsNotNull(registers);
Assert.AreEqual(2, registers.Count); Assert.HasCount(2, registers);
Assert.AreEqual(5, registers[0].Address); Assert.AreEqual(5, registers[0].Address);
Assert.AreEqual(0x41, registers[0].HighByte); Assert.AreEqual(0x41, registers[0].HighByte);
@@ -252,7 +228,7 @@
// Assert // Assert
Assert.IsNotNull(registers); Assert.IsNotNull(registers);
Assert.AreEqual(2, registers.Count); Assert.HasCount(2, registers);
Assert.AreEqual(6, registers[0].Address); Assert.AreEqual(6, registers[0].Address);
Assert.AreEqual(0x41, registers[0].HighByte); Assert.AreEqual(0x41, registers[0].HighByte);
@@ -274,7 +250,7 @@
// Assert // Assert
Assert.IsNotNull(registers); Assert.IsNotNull(registers);
Assert.AreEqual(4, registers.Count); Assert.HasCount(4, registers);
Assert.AreEqual(5, registers[0].Address); Assert.AreEqual(5, registers[0].Address);
Assert.AreEqual(0x40, registers[0].HighByte); Assert.AreEqual(0x40, registers[0].HighByte);
@@ -304,7 +280,7 @@
// Assert // Assert
Assert.IsNotNull(registers); Assert.IsNotNull(registers);
Assert.AreEqual(4, registers.Count); Assert.HasCount(4, registers);
Assert.AreEqual(8, registers[0].Address); Assert.AreEqual(8, registers[0].Address);
Assert.AreEqual(0x40, registers[0].HighByte); Assert.AreEqual(0x40, registers[0].HighByte);

View File

@@ -30,16 +30,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowNullOnGetBoolean() public void ShouldThrowNullOnGetBoolean()
{ {
// Arrange // Arrange
Coil coil = null; Coil coil = null;
// Act // Act + Assert
coil.GetBoolean(); Assert.ThrowsExactly<ArgumentNullException>(() => coil.GetBoolean());
// Assert - ArgumentNullException
} }
[TestMethod] [TestMethod]
@@ -95,35 +92,28 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowNullOnString() public void ShouldThrowNullOnString()
{ {
// Arrange // Arrange
HoldingRegister[] list = null; HoldingRegister[] list = null;
// Act // Act + Assert
list.GetString(2); Assert.ThrowsExactly<ArgumentNullException>(() => list.GetString(2));
// Assert - ArgumentNullException
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentOnStringForEmptyList() public void ShouldThrowArgumentOnStringForEmptyList()
{ {
// Arrange // Arrange
var registers = Array.Empty<HoldingRegister>(); var registers = Array.Empty<HoldingRegister>();
// Act // Act + Assert
registers.GetString(2); Assert.ThrowsExactly<ArgumentException>(() => registers.GetString(2));
// Assert - ArgumentException
} }
[DataTestMethod] [TestMethod]
[DataRow(1)] [DataRow(1)]
[DataRow(-1)] [DataRow(-1)]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ShouldThrowArgumentOutOfRangeOnString(int startIndex) public void ShouldThrowArgumentOutOfRangeOnString(int startIndex)
{ {
// Arrange // Arrange
@@ -133,14 +123,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
new() { Address = 2, HighByte = 67, LowByte = 0 } new() { Address = 2, HighByte = 67, LowByte = 0 }
}; };
// Act // Act + Assert
registers.GetString(2, startIndex); Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => registers.GetString(2, startIndex));
// Assert - ArgumentOutOfRangeException
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentOnStringForMixedTypes() public void ShouldThrowArgumentOnStringForMixedTypes()
{ {
// Arrange // Arrange
@@ -150,10 +137,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
new InputRegister { Address = 2, HighByte = 67, LowByte = 0 } new InputRegister { Address = 2, HighByte = 67, LowByte = 0 }
}; };
// Act // Act + Assert
registers.GetString(2); Assert.ThrowsExactly<ArgumentException>(() => registers.GetString(2));
// Assert - ArgumentException
} }
#endregion Modbus to value #endregion Modbus to value
@@ -187,7 +172,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
// Assert // Assert
Assert.IsNotNull(register); Assert.IsNotNull(register);
Assert.AreEqual(321, register.Address); Assert.AreEqual(321, register.Address);
Assert.IsTrue(register.Value > 0); Assert.IsGreaterThan(0, register.Value);
} }
[TestMethod] [TestMethod]
@@ -202,7 +187,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
// Assert // Assert
Assert.IsNotNull(register); Assert.IsNotNull(register);
Assert.AreEqual(321, register.Address); Assert.AreEqual(321, register.Address);
Assert.IsTrue(register.Value == 0); Assert.AreEqual(0, register.Value);
} }
[TestMethod] [TestMethod]
@@ -216,7 +201,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
// Assert // Assert
Assert.IsNotNull(registers); Assert.IsNotNull(registers);
Assert.AreEqual(2, registers.Count); Assert.HasCount(2, registers);
Assert.AreEqual(100, registers[0].Address); Assert.AreEqual(100, registers[0].Address);
Assert.AreEqual(97, registers[0].HighByte); Assert.AreEqual(97, registers[0].HighByte);
@@ -238,7 +223,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
// Assert // Assert
Assert.IsNotNull(registers); Assert.IsNotNull(registers);
Assert.AreEqual(2, registers.Count); Assert.HasCount(2, registers);
Assert.AreEqual(101, registers[0].Address); Assert.AreEqual(101, registers[0].Address);
Assert.AreEqual(97, registers[0].HighByte); Assert.AreEqual(97, registers[0].HighByte);
@@ -260,7 +245,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
// Assert // Assert
Assert.IsNotNull(registers); Assert.IsNotNull(registers);
Assert.AreEqual(2, registers.Count); Assert.HasCount(2, registers);
Assert.AreEqual(100, registers[0].Address); Assert.AreEqual(100, registers[0].Address);
Assert.AreEqual(97, registers[0].LowByte); Assert.AreEqual(97, registers[0].LowByte);
@@ -272,16 +257,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowNullOnGetString() public void ShouldThrowNullOnGetString()
{ {
// Arrange // Arrange
string str = null; string str = null;
// Act // Act + Assert
_ = str.ToRegisters(100).ToArray(); Assert.ThrowsExactly<ArgumentNullException>(() => str.ToRegisters(100).ToArray());
// Assert - ArgumentNullException
} }
#endregion Value to Modbus #endregion Value to Modbus

View File

@@ -32,31 +32,23 @@
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowNullForGetSByte() public void ShouldThrowNullForGetSByte()
{ {
// Arrange // Arrange
HoldingRegister register = null; HoldingRegister register = null;
// Act // Act + Assert
register.GetSByte(); Assert.ThrowsExactly<ArgumentNullException>(() => register.GetSByte());
// Assert - ArgumentNullException
Assert.Fail();
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentForGetSByte() public void ShouldThrowArgumentForGetSByte()
{ {
// Arrange // Arrange
var obj = new Coil(); var obj = new Coil();
// Act // Act + Assert
obj.GetSByte(); Assert.ThrowsExactly<ArgumentException>(() => obj.GetSByte());
// Assert - ArgumentException
Assert.Fail();
} }
[TestMethod] [TestMethod]
@@ -86,31 +78,23 @@
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowNullForGetInt16() public void ShouldThrowNullForGetInt16()
{ {
// Arrange // Arrange
HoldingRegister register = null; HoldingRegister register = null;
// Act // Act + Assert
register.GetInt16(); Assert.ThrowsExactly<ArgumentNullException>(() => register.GetInt16());
// Assert - ArgumentNullException
Assert.Fail();
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentForGetInt16() public void ShouldThrowArgumentForGetInt16()
{ {
// Arrange // Arrange
var obj = new Coil(); var obj = new Coil();
// Act // Act + Assert
obj.GetInt16(); Assert.ThrowsExactly<ArgumentException>(() => obj.GetInt16());
// Assert - ArgumentException
Assert.Fail();
} }
[TestMethod] [TestMethod]
@@ -119,9 +103,9 @@
// Arrange // Arrange
var registers = new HoldingRegister[] var registers = new HoldingRegister[]
{ {
new HoldingRegister(), new(),
new HoldingRegister { Address = 100, HighByte = 0x01, LowByte = 0x02 }, new() { Address = 100, HighByte = 0x01, LowByte = 0x02 },
new HoldingRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 } new() { Address = 101, HighByte = 0x03, LowByte = 0x04 }
}; };
// Act // Act
@@ -137,8 +121,8 @@
// Arrange // Arrange
var registers = new HoldingRegister[] var registers = new HoldingRegister[]
{ {
new HoldingRegister { Address = 101, HighByte = 0x01, LowByte = 0x02 }, new() { Address = 101, HighByte = 0x01, LowByte = 0x02 },
new HoldingRegister { Address = 100, HighByte = 0x03, LowByte = 0x04 } new() { Address = 100, HighByte = 0x03, LowByte = 0x04 }
}; };
// Act // Act
@@ -149,58 +133,45 @@
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowNullOnGetInt32() public void ShouldThrowNullOnGetInt32()
{ {
// Arrange // Arrange
HoldingRegister[] registers = null; HoldingRegister[] registers = null;
// Act // Act + Assert
registers.GetInt32(0); Assert.ThrowsExactly<ArgumentNullException>(() => registers.GetInt32(0));
// Assert - ArgumentNullException
Assert.Fail();
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentOnGetInt32ForLength() public void ShouldThrowArgumentOnGetInt32ForLength()
{ {
// Arrange // Arrange
var registers = new HoldingRegister[] var registers = new HoldingRegister[]
{ {
new HoldingRegister { Address = 101, HighByte = 0x01, LowByte = 0x02 } new() { Address = 101, HighByte = 0x01, LowByte = 0x02 }
}; };
// Act // Act + Assert
registers.GetInt32(0); Assert.ThrowsExactly<ArgumentException>(() => registers.GetInt32(0));
// Assert - ArgumentException
Assert.Fail();
} }
[DataTestMethod] [TestMethod]
[DataRow(1)] [DataRow(1)]
[DataRow(-1)] [DataRow(-1)]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ShouldThrowArgumentOutOfRangeOnGetInt32(int startIndex) public void ShouldThrowArgumentOutOfRangeOnGetInt32(int startIndex)
{ {
// Arrange // Arrange
var registers = new HoldingRegister[] var registers = new HoldingRegister[]
{ {
new HoldingRegister { Address = 101, HighByte = 0x01, LowByte = 0x02 }, new() { Address = 101, HighByte = 0x01, LowByte = 0x02 },
new HoldingRegister { Address = 100, HighByte = 0x03, LowByte = 0x04 } new() { Address = 100, HighByte = 0x03, LowByte = 0x04 }
}; };
// Act // Act + Assert
registers.GetInt32(startIndex); Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => registers.GetInt32(startIndex));
// Assert - ArgumentOutOfRangeException
Assert.Fail();
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentOnGetInt32ForType() public void ShouldThrowArgumentOnGetInt32ForType()
{ {
// Arrange // Arrange
@@ -210,11 +181,8 @@
new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 } new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 }
}; };
// Act // Act + Assert
registers.GetInt32(0); Assert.ThrowsExactly<ArgumentException>(() => registers.GetInt32(0));
// Assert - ArgumentException
Assert.Fail();
} }
[TestMethod] [TestMethod]
@@ -223,11 +191,11 @@
// Arrange // Arrange
var registers = new HoldingRegister[] var registers = new HoldingRegister[]
{ {
new HoldingRegister(), new(),
new HoldingRegister { Address = 100, HighByte = 0x00, LowByte = 0x00 }, new() { Address = 100, HighByte = 0x00, LowByte = 0x00 },
new HoldingRegister { Address = 101, HighByte = 0x00, LowByte = 0x00 }, new() { Address = 101, HighByte = 0x00, LowByte = 0x00 },
new HoldingRegister { Address = 102, HighByte = 0x01, LowByte = 0x02 }, new() { Address = 102, HighByte = 0x01, LowByte = 0x02 },
new HoldingRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 } new() { Address = 103, HighByte = 0x03, LowByte = 0x04 }
}; };
// Act // Act
@@ -243,10 +211,10 @@
// Arrange // Arrange
var registers = new HoldingRegister[] var registers = new HoldingRegister[]
{ {
new HoldingRegister { Address = 103, HighByte = 0x00, LowByte = 0x00 }, new() { Address = 103, HighByte = 0x00, LowByte = 0x00 },
new HoldingRegister { Address = 102, HighByte = 0x00, LowByte = 0x00 }, new() { Address = 102, HighByte = 0x00, LowByte = 0x00 },
new HoldingRegister { Address = 101, HighByte = 0x01, LowByte = 0x02 }, new() { Address = 101, HighByte = 0x01, LowByte = 0x02 },
new HoldingRegister { Address = 100, HighByte = 0x03, LowByte = 0x04 } new() { Address = 100, HighByte = 0x03, LowByte = 0x04 }
}; };
// Act // Act
@@ -257,62 +225,49 @@
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowNullOnGetInt64() public void ShouldThrowNullOnGetInt64()
{ {
// Arrange // Arrange
HoldingRegister[] registers = null; HoldingRegister[] registers = null;
// Act // Act + Assert
registers.GetInt64(0); Assert.ThrowsExactly<ArgumentNullException>(() => registers.GetInt64(0));
// Assert - ArgumentNullException
Assert.Fail();
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentOnGetInt64ForLength() public void ShouldThrowArgumentOnGetInt64ForLength()
{ {
// Arrange // Arrange
var registers = new HoldingRegister[] var registers = new HoldingRegister[]
{ {
new HoldingRegister { Address = 101, HighByte = 0x00, LowByte = 0x00 }, new() { Address = 101, HighByte = 0x00, LowByte = 0x00 },
new HoldingRegister { Address = 102, HighByte = 0x01, LowByte = 0x02 }, new() { Address = 102, HighByte = 0x01, LowByte = 0x02 },
new HoldingRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 } new() { Address = 103, HighByte = 0x03, LowByte = 0x04 }
}; };
// Act // Act + Assert
registers.GetInt64(0); Assert.ThrowsExactly<ArgumentException>(() => registers.GetInt64(0));
// Assert - ArgumentException
Assert.Fail();
} }
[DataTestMethod] [TestMethod]
[DataRow(1)] [DataRow(1)]
[DataRow(-1)] [DataRow(-1)]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ShouldThrowArgumentOutOfRangeOnGetInt64(int startIndex) public void ShouldThrowArgumentOutOfRangeOnGetInt64(int startIndex)
{ {
// Arrange // Arrange
var registers = new HoldingRegister[] var registers = new HoldingRegister[]
{ {
new HoldingRegister { Address = 100, HighByte = 0x00, LowByte = 0x00 }, new() { Address = 100, HighByte = 0x00, LowByte = 0x00 },
new HoldingRegister { Address = 101, HighByte = 0x00, LowByte = 0x00 }, new() { Address = 101, HighByte = 0x00, LowByte = 0x00 },
new HoldingRegister { Address = 102, HighByte = 0x01, LowByte = 0x02 }, new() { Address = 102, HighByte = 0x01, LowByte = 0x02 },
new HoldingRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 } new() { Address = 103, HighByte = 0x03, LowByte = 0x04 }
}; };
// Act // Act + Assert
registers.GetInt64(startIndex); Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => registers.GetInt64(startIndex));
// Assert - ArgumentOutOfRangeException
Assert.Fail();
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentOnGetInt64ForType() public void ShouldThrowArgumentOnGetInt64ForType()
{ {
// Arrange // Arrange
@@ -324,11 +279,8 @@
new InputRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 } new InputRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
}; };
// Act // Act + Assert
registers.GetInt64(0); Assert.ThrowsExactly<ArgumentException>(() => registers.GetInt64(0));
// Assert - ArgumentException
Assert.Fail();
} }
#endregion Modbus to value #endregion Modbus to value
@@ -378,7 +330,7 @@
// Assert // Assert
Assert.IsNotNull(registers); Assert.IsNotNull(registers);
Assert.AreEqual(2, registers.Count); Assert.HasCount(2, registers);
Assert.AreEqual(5, registers[0].Address); Assert.AreEqual(5, registers[0].Address);
Assert.AreEqual(0x00, registers[0].HighByte); Assert.AreEqual(0x00, registers[0].HighByte);
@@ -400,7 +352,7 @@
// Assert // Assert
Assert.IsNotNull(registers); Assert.IsNotNull(registers);
Assert.AreEqual(2, registers.Count); Assert.HasCount(2, registers);
Assert.AreEqual(6, registers[0].Address); Assert.AreEqual(6, registers[0].Address);
Assert.AreEqual(0x00, registers[0].HighByte); Assert.AreEqual(0x00, registers[0].HighByte);
@@ -422,7 +374,7 @@
// Assert // Assert
Assert.IsNotNull(registers); Assert.IsNotNull(registers);
Assert.AreEqual(4, registers.Count); Assert.HasCount(4, registers);
Assert.AreEqual(10, registers[0].Address); Assert.AreEqual(10, registers[0].Address);
Assert.AreEqual(0x00, registers[0].HighByte); Assert.AreEqual(0x00, registers[0].HighByte);
@@ -452,7 +404,7 @@
// Assert // Assert
Assert.IsNotNull(registers); Assert.IsNotNull(registers);
Assert.AreEqual(4, registers.Count); Assert.HasCount(4, registers);
Assert.AreEqual(13, registers[0].Address); Assert.AreEqual(13, registers[0].Address);
Assert.AreEqual(0x00, registers[0].HighByte); Assert.AreEqual(0x00, registers[0].HighByte);

View File

@@ -32,29 +32,23 @@
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowNullForGetByte() public void ShouldThrowNullForGetByte()
{ {
// Arrange // Arrange
HoldingRegister register = null; HoldingRegister register = null;
// Act // Act + Assert
register.GetByte(); Assert.ThrowsExactly<ArgumentNullException>(() => register.GetByte());
// Assert - ArgumentNullException
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentForGetByte() public void ShouldThrowArgumentForGetByte()
{ {
// Arrange // Arrange
var obj = new Coil(); var obj = new Coil();
// Act // Act + Assert
obj.GetByte(); Assert.ThrowsExactly<ArgumentException>(() => obj.GetByte());
// Assert - ArgumentException
} }
[TestMethod] [TestMethod]
@@ -84,29 +78,23 @@
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowNullForGetUInt16() public void ShouldThrowNullForGetUInt16()
{ {
// Arrange // Arrange
HoldingRegister register = null; HoldingRegister register = null;
// Act // Act + Assert
register.GetUInt16(); Assert.ThrowsExactly<ArgumentNullException>(() => register.GetUInt16());
// Assert - ArgumentNullException
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentForGetUInt16() public void ShouldThrowArgumentForGetUInt16()
{ {
// Arrange // Arrange
var obj = new Coil(); var obj = new Coil();
// Act // Act + Assert
obj.GetUInt16(); Assert.ThrowsExactly<ArgumentException>(() => obj.GetUInt16());
// Assert - ArgumentException
} }
[TestMethod] [TestMethod]
@@ -145,21 +133,16 @@
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowNullOnGetUInt32() public void ShouldThrowNullOnGetUInt32()
{ {
// Arrange // Arrange
HoldingRegister[] registers = null; HoldingRegister[] registers = null;
// Act // Act + Assert
registers.GetUInt32(0); Assert.ThrowsExactly<ArgumentNullException>(() => registers.GetUInt32(0));
// Assert - ArgumentNullException
Assert.Fail();
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentOnGetUInt32ForLength() public void ShouldThrowArgumentOnGetUInt32ForLength()
{ {
// Arrange // Arrange
@@ -168,16 +151,13 @@
new() { Address = 101, HighByte = 0x01, LowByte = 0x02 } new() { Address = 101, HighByte = 0x01, LowByte = 0x02 }
}; };
// Act // Act + Assert
registers.GetUInt32(0); Assert.ThrowsExactly<ArgumentException>(() => registers.GetUInt32(1));
// Assert - ArgumentException
} }
[DataTestMethod] [TestMethod]
[DataRow(1)] [DataRow(1)]
[DataRow(-1)] [DataRow(-1)]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ShouldThrowArgumentOutOfRangeOnGetUInt32(int startIndex) public void ShouldThrowArgumentOutOfRangeOnGetUInt32(int startIndex)
{ {
// Arrange // Arrange
@@ -187,14 +167,11 @@
new() { Address = 100, HighByte = 0x03, LowByte = 0x04 } new() { Address = 100, HighByte = 0x03, LowByte = 0x04 }
}; };
// Act // Act + Assert
registers.GetUInt32(startIndex); Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => registers.GetUInt32(startIndex));
// Assert - ArgumentOutOfRangeException
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentOnGetUInt32ForType() public void ShouldThrowArgumentOnGetUInt32ForType()
{ {
// Arrange // Arrange
@@ -204,10 +181,8 @@
new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 } new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 }
}; };
// Act // Act + Assert
registers.GetUInt32(0); Assert.ThrowsExactly<ArgumentException>(() => registers.GetUInt32(0));
// Assert - ArgumentException
} }
[TestMethod] [TestMethod]
@@ -250,21 +225,16 @@
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowNullOnGetUInt64() public void ShouldThrowNullOnGetUInt64()
{ {
// Arrange // Arrange
HoldingRegister[] registers = null; HoldingRegister[] registers = null;
// Act // Act + Assert
registers.GetUInt64(0); Assert.ThrowsExactly<ArgumentNullException>(() => registers.GetUInt64(0));
// Assert - ArgumentNullException
Assert.Fail();
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentOnGetUInt64ForLength() public void ShouldThrowArgumentOnGetUInt64ForLength()
{ {
// Arrange // Arrange
@@ -275,16 +245,13 @@
new() { Address = 103, HighByte = 0x03, LowByte = 0x04 } new() { Address = 103, HighByte = 0x03, LowByte = 0x04 }
}; };
// Act // Act + Assert
registers.GetUInt64(0); Assert.ThrowsExactly<ArgumentException>(() => registers.GetUInt64(0));
// Assert - ArgumentException
} }
[DataTestMethod] [TestMethod]
[DataRow(1)] [DataRow(1)]
[DataRow(-1)] [DataRow(-1)]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ShouldThrowArgumentOutOfRangeOnGetUInt64(int startIndex) public void ShouldThrowArgumentOutOfRangeOnGetUInt64(int startIndex)
{ {
// Arrange // Arrange
@@ -296,14 +263,11 @@
new() { Address = 103, HighByte = 0x03, LowByte = 0x04 } new() { Address = 103, HighByte = 0x03, LowByte = 0x04 }
}; };
// Act // Act + Assert
registers.GetUInt64(startIndex); Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => registers.GetUInt64(startIndex));
// Assert - ArgumentOutOfRangeException
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentOnGetUInt64ForType() public void ShouldThrowArgumentOnGetUInt64ForType()
{ {
// Arrange // Arrange
@@ -315,10 +279,8 @@
new InputRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 } new InputRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
}; };
// Act // Act + Assert
registers.GetUInt64(0); Assert.ThrowsExactly<ArgumentException>(() => registers.GetUInt64(0));
// Assert - ArgumentException
} }
#endregion Modbus to value #endregion Modbus to value
@@ -368,7 +330,7 @@
// Assert // Assert
Assert.IsNotNull(registers); Assert.IsNotNull(registers);
Assert.AreEqual(2, registers.Count); Assert.HasCount(2, registers);
Assert.AreEqual(5, registers[0].Address); Assert.AreEqual(5, registers[0].Address);
Assert.AreEqual(0x00, registers[0].HighByte); Assert.AreEqual(0x00, registers[0].HighByte);
@@ -390,7 +352,7 @@
// Assert // Assert
Assert.IsNotNull(registers); Assert.IsNotNull(registers);
Assert.AreEqual(2, registers.Count); Assert.HasCount(2, registers);
Assert.AreEqual(6, registers[0].Address); Assert.AreEqual(6, registers[0].Address);
Assert.AreEqual(0x00, registers[0].HighByte); Assert.AreEqual(0x00, registers[0].HighByte);
@@ -412,7 +374,7 @@
// Assert // Assert
Assert.IsNotNull(registers); Assert.IsNotNull(registers);
Assert.AreEqual(4, registers.Count); Assert.HasCount(4, registers);
Assert.AreEqual(10, registers[0].Address); Assert.AreEqual(10, registers[0].Address);
Assert.AreEqual(0x00, registers[0].HighByte); Assert.AreEqual(0x00, registers[0].HighByte);
@@ -442,7 +404,7 @@
// Assert // Assert
Assert.IsNotNull(registers); Assert.IsNotNull(registers);
Assert.AreEqual(4, registers.Count); Assert.HasCount(4, registers);
Assert.AreEqual(13, registers[0].Address); Assert.AreEqual(13, registers[0].Address);
Assert.AreEqual(0x00, registers[0].HighByte); Assert.AreEqual(0x00, registers[0].HighByte);

Some files were not shown because too many files have changed in this diff Show More