16 Commits

Author SHA1 Message Date
42af5a06f2 Updated to .NET 10
Some checks failed
Branch Build / build-test-deploy (push) Failing after 1m12s
2026-01-12 18:32:15 +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
133 changed files with 2031 additions and 2063 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'
CI_SERVER_HOST: ${{ 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: 8.x
cache: false
- name: Restore dependencies
run: |
set -ex
dotnet restore -v q
dotnet tool restore -v q
- 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'
CI_SERVER_HOST: ${{ 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: 8.x
cache: false
- name: Restore dependencies
run: |
set -ex
dotnet restore -v q
dotnet tool restore -v q
- 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,134 +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:
- shopt -s globstar
- mkdir ./artifacts
- dotnet restore --no-cache --force
- dotnet build -c Debug --nologo --no-restore --no-incremental
- mv ./**/*.nupkg ./artifacts/
- mv ./**/*.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: /Branch coverage[\s\S].+%/
before_script:
- dotnet tool install dotnet-reportgenerator-globaltool --tool-path /dotnet-tools
script:
- dotnet test -c Debug --nologo /p:CoverletOutputFormat=Cobertura
- /dotnet-tools/reportgenerator "-reports:${CI_PROJECT_DIR}/**/coverage.cobertura.xml" "-targetdir:/reports" -reportType:TextSummary
after_script:
- cat /reports/Summary.txt
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: ./**/coverage.cobertura.xml
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:
- shopt -s globstar
- mkdir ./artifacts
- dotnet restore --no-cache --force
- dotnet build -c Release --nologo --no-restore --no-incremental
- mv ./**/*.nupkg ./artifacts/
- mv ./**/*.snupkg ./artifacts/
artifacts:
paths:
- artifacts/*.nupkg
- artifacts/*.snupkg
expire_in: 7 days
test-release:
stage: test
dependencies:
- build-release
tags:
- docker
- lnx
- amd64
rules:
- if: $CI_COMMIT_TAG != null
coverage: /Branch coverage[\s\S].+%/
before_script:
- dotnet tool install dotnet-reportgenerator-globaltool --tool-path /dotnet-tools
script:
- dotnet test -c Release --nologo /p:CoverletOutputFormat=Cobertura
- /dotnet-tools/reportgenerator "-reports:${CI_PROJECT_DIR}/**/coverage.cobertura.xml" "-targetdir:/reports" -reportType:TextSummary
after_script:
- cat /reports/Summary.txt
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: ./**/coverage.cobertura.xml
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,30 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
<PackageId>AMWD.Protocols.Modbus.Tcp</PackageId>
<AssemblyName>amwd-modbus-tcp</AssemblyName>
<RootNamespace>AMWD.Protocols.Modbus.Tcp</RootNamespace>
<Product>Modbus TCP Protocol</Product>
<Description>Implementation of the Modbus protocol communicating via TCP.</Description>
<PackageTags>Modbus Protocol Network TCP LAN</PackageTags>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(SolutionDir)/AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" />
<Compile Include="$(SolutionDir)/AMWD.Protocols.Modbus.Common/Extensions/ReaderWriterLockSlimExtensions.cs" Link="Extensions/ReaderWriterLockSlimExtensions.cs" />
<Compile Include="$(SolutionDir)/AMWD.Protocols.Modbus.Common/Utils/AsyncQueue.cs" Link="Utils/AsyncQueue.cs" />
<Compile Include="$(SolutionDir)/AMWD.Protocols.Modbus.Common/Utils/RequestQueueItem.cs" Link="Utils/RequestQueueItem.cs" />
</ItemGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="/" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(SolutionDir)\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,32 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<CollectCoverage>true</CollectCoverage>
<CoverletOutputFormat>Cobertura</CoverletOutputFormat>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.msbuild" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="MSTest.TestAdapter" Version="3.7.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.7.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(SolutionDir)\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
<ProjectReference Include="$(SolutionDir)\AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj" />
<ProjectReference Include="$(SolutionDir)\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,84 +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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliClient", "CliClient\CliClient.csproj", "{B0E53462-B0ED-4685-8AA5-948DC160EE27}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliProxy", "CliProxy\CliProxy.csproj", "{AC922E80-E9B6-493D-B1D1-752527E883ED}"
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
{B0E53462-B0ED-4685-8AA5-948DC160EE27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B0E53462-B0ED-4685-8AA5-948DC160EE27}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B0E53462-B0ED-4685-8AA5-948DC160EE27}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B0E53462-B0ED-4685-8AA5-948DC160EE27}.Release|Any CPU.Build.0 = Release|Any CPU
{AC922E80-E9B6-493D-B1D1-752527E883ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AC922E80-E9B6-493D-B1D1-752527E883ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AC922E80-E9B6-493D-B1D1-752527E883ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AC922E80-E9B6-493D-B1D1-752527E883ED}.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,37 @@
<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/">
<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>

View File

@@ -7,7 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
_nothing changed yet_ ### 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) ## [v0.4.0] (2025-01-29)
@@ -77,7 +105,9 @@ So this tag is only here for documentation purposes of the NuGet Gallery.
[Unreleased]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.4.0...HEAD [Unreleased]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.4.2...HEAD
[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.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.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.1]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.0...v0.3.1

View File

@@ -1,33 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>modbus-client</AssemblyName>
<RootNamespace>AMWD.Protocols.Modbus.CliClient</RootNamespace>
<Product>Modbus CLI client</Product>
<Description>Small CLI client for Modbus communication.</Description>
<IsPackable>false</IsPackable>
<SignAssembly>false</SignAssembly>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
<None Remove="$(SolutionDir)/package-icon.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(SolutionDir)\AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj" />
<ProjectReference Include="$(SolutionDir)\AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,33 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>modbus-proxy</AssemblyName>
<RootNamespace>AMWD.Protocols.Modbus.CliProxy</RootNamespace>
<Product>Modbus CLI proxy</Product>
<Description>Small CLI proxy to forward messages.</Description>
<IsPackable>false</IsPackable>
<SignAssembly>false</SignAssembly>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
<None Remove="$(SolutionDir)/package-icon.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(SolutionDir)\AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj" />
<ProjectReference Include="$(SolutionDir)\AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,61 +1,20 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<LangVersion>12.0</LangVersion> <LangVersion>14.0</LangVersion>
<NrtRevisionFormat>{semvertag:main}{!:-dev}</NrtRevisionFormat> <NrtRevisionFormat>{semvertag:main}{!:-dev}</NrtRevisionFormat>
<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>
<EmbedUntrackedSources>false</EmbedUntrackedSources>
<PackageIcon>package-icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<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> <SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>$(SolutionDir)/AMWD.Protocols.Modbus.snk</AssemblyOriginatorKeyFile> <PublicKey>0024000004800000940000000602000000240000525341310004000001000100adcc4f9f5bb3ac73cb30661f6f35772b8f90a74412925764a960af06ef125bdcec05ed1d139503d5203fb72aa3fa74bab58e82ac2a6cd4b650f8cbf7086a71bc2dfc67e95b8d26d776d60856acf3121f831529b1a4dee91b34ac84f95f71a1165b7783edb591929ba2a684100c92bbed8859c7266fb507f6f55bb6f7fcac80b4</PublicKey>
<MoqPublicKey>0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7</MoqPublicKey>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(GITLAB_CI)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup Condition="'$(SignAssembly)' != 'true'">
<InternalsVisibleTo Include="AMWD.Protocols.Modbus.Tests" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
</ItemGroup>
<ItemGroup Condition="'$(SignAssembly)' == 'true'">
<InternalsVisibleTo Include="AMWD.Protocols.Modbus.Tests" PublicKey="0024000004800000940000000602000000240000525341310004000001000100adcc4f9f5bb3ac73cb30661f6f35772b8f90a74412925764a960af06ef125bdcec05ed1d139503d5203fb72aa3fa74bab58e82ac2a6cd4b650f8cbf7086a71bc2dfc67e95b8d26d776d60856acf3121f831529b1a4dee91b34ac84f95f71a1165b7783edb591929ba2a684100c92bbed8859c7266fb507f6f55bb6f7fcac80b4" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" PublicKey="0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" />
</ItemGroup>
<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="$(SolutionDir)/package-icon.png" Pack="true" PackagePath="/" /> <PackageReference Include="AMWD.NetRevisionTask" Version="1.4.0">
</ItemGroup>
<ItemGroup>
<PackageReference Include="AMWD.NetRevisionTask" Version="1.2.1">
<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

@@ -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]).
@@ -34,15 +36,13 @@ It uses a specific TCP connection implementation and plugs all things from the C
--- ---
Published under [MIT License] (see [choose a license]) 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
[Serial]: AMWD.Protocols.Modbus.Serial/README.md [Serial]: src/AMWD.Protocols.Modbus.Serial/README.md
[TCP]: AMWD.Protocols.Modbus.Tcp/README.md [TCP]: src/AMWD.Protocols.Modbus.Tcp/README.md
[MIT License]: LICENSE.txt [MIT License]: LICENSE.txt
[choose a license]: https://choosealicense.com/licenses/mit/ [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,8 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
<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>
@@ -12,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

@@ -10,19 +10,27 @@ 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

@@ -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>
@@ -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.");

View File

@@ -71,7 +71,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
public DeviceIdentificationRaw DeserializeReadDeviceIdentification(IReadOnlyList<byte> response) public DeviceIdentificationRaw DeserializeReadDeviceIdentification(IReadOnlyList<byte> response)
{ {
if (!_devices.TryGetValue(response[0], out var device)) if (!_devices.TryGetValue(response[0], out var _))
throw new TimeoutException("Device not found."); throw new TimeoutException("Device not found.");
var result = new DeviceIdentificationRaw var result = new DeviceIdentificationRaw

View File

@@ -1,8 +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>
<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>
@@ -13,14 +12,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="$(SolutionDir)/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="$(SolutionDir)/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="$(SolutionDir)/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="$(SolutionDir)/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>
<None Include="README.md" Pack="true" PackagePath="/" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'"> <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
@@ -35,8 +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="$(SolutionDir)\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" /> <ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -192,17 +192,16 @@ namespace AMWD.Protocols.Modbus.Serial
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>
@@ -215,7 +214,7 @@ namespace AMWD.Protocols.Modbus.Serial
_isDisposed = true; _isDisposed = true;
StopAsyncInternal(CancellationToken.None).Wait(); StopAsyncInternal();
_serialPort.Dispose(); _serialPort.Dispose();
_stopCts?.Dispose(); _stopCts?.Dispose();
@@ -332,7 +331,7 @@ namespace AMWD.Protocols.Modbus.Serial
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++)
@@ -371,7 +370,7 @@ namespace AMWD.Protocols.Modbus.Serial
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++)
@@ -410,7 +409,7 @@ namespace AMWD.Protocols.Modbus.Serial
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++)
@@ -444,7 +443,7 @@ namespace AMWD.Protocols.Modbus.Serial
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++)
@@ -492,7 +491,7 @@ namespace AMWD.Protocols.Modbus.Serial
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
@@ -531,7 +530,7 @@ namespace AMWD.Protocols.Modbus.Serial
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
@@ -591,7 +590,7 @@ namespace AMWD.Protocols.Modbus.Serial
}); });
} }
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
@@ -648,7 +647,7 @@ namespace AMWD.Protocols.Modbus.Serial
}); });
} }
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
@@ -705,7 +704,7 @@ namespace AMWD.Protocols.Modbus.Serial
try try
{ {
var deviceInfo = await Client.ReadDeviceIdentificationAsync(requestBytes[0], 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>();
@@ -855,5 +854,21 @@ namespace AMWD.Protocols.Modbus.Serial
} }
#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

@@ -31,8 +31,7 @@ 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
@@ -41,6 +40,8 @@ namespace AMWD.Protocols.Modbus.Serial
/// </summary> /// </summary>
public ModbusSerialConnection(string portName) public ModbusSerialConnection(string portName)
{ {
_isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
if (string.IsNullOrWhiteSpace(portName)) if (string.IsNullOrWhiteSpace(portName))
throw new ArgumentNullException(nameof(portName)); throw new ArgumentNullException(nameof(portName));
@@ -268,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();
@@ -276,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();
@@ -291,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();
@@ -322,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)
@@ -353,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;
@@ -361,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;
@@ -379,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

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageId>AMWD.Protocols.Modbus.Tcp</PackageId>
<AssemblyName>amwd-modbus-tcp</AssemblyName>
<RootNamespace>AMWD.Protocols.Modbus.Tcp</RootNamespace>
<Product>Modbus TCP Protocol</Product>
<Description>Implementation of the Modbus protocol communicating via TCP.</Description>
<PackageTags>Modbus Protocol Network TCP LAN</PackageTags>
</PropertyGroup>
<ItemGroup>
<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/Utils/AsyncQueue.cs" Link="Utils/AsyncQueue.cs" />
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/RequestQueueItem.cs" Link="Utils/RequestQueueItem.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -12,7 +12,7 @@ namespace System.IO
int offset = 0; int offset = 0;
do do
{ {
int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken); int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (count < 1) if (count < 1)
throw new EndOfStreamException(); throw new EndOfStreamException();
@@ -30,7 +30,7 @@ namespace System.IO
int offset = 0; int offset = 0;
do do
{ {
int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken); int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (count < 1) if (count < 1)
throw new EndOfStreamException(); throw new EndOfStreamException();

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

@@ -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,6 +10,7 @@ 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; using AMWD.Protocols.Modbus.Tcp.Utils;
namespace AMWD.Protocols.Modbus.Tcp namespace AMWD.Protocols.Modbus.Tcp
@@ -17,7 +18,12 @@ 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 : IModbusProxy /// <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
@@ -25,30 +31,17 @@ namespace AMWD.Protocols.Modbus.Tcp
private TimeSpan _readWriteTimeout = TimeSpan.FromSeconds(100); private TimeSpan _readWriteTimeout = TimeSpan.FromSeconds(100);
private TcpListenerWrapper _tcpListener; 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<TcpClientWrapper> _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.</param>
public ModbusTcpProxy(ModbusClientBase client, IPAddress listenAddress)
{
Client = client ?? throw new ArgumentNullException(nameof(client));
_tcpListener = new TcpListenerWrapper(listenAddress, 502);
}
#endregion Constructors #endregion Constructors
#region Properties #region Properties
@@ -56,7 +49,7 @@ namespace AMWD.Protocols.Modbus.Tcp
/// <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.
@@ -115,7 +108,10 @@ namespace AMWD.Protocols.Modbus.Tcp
_stopCts?.Dispose(); _stopCts?.Dispose();
_stopCts = new CancellationTokenSource(); _stopCts = new CancellationTokenSource();
_tcpListener.Socket.DualMode = ListenAddress.AddressFamily == AddressFamily.InterNetworkV6; // 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)
_tcpListener.Socket.DualMode = true;
_tcpListener.Start(); _tcpListener.Start();
_clientConnectTask = WaitForClientAsync(_stopCts.Token); _clientConnectTask = WaitForClientAsync(_stopCts.Token);
@@ -140,16 +136,7 @@ namespace AMWD.Protocols.Modbus.Tcp
try try
{ {
await Task.WhenAny(_clientConnectTask, Task.Delay(Timeout.Infinite, cancellationToken)); await Task.WhenAny(_clientConnectTask, Task.Delay(Timeout.Infinite, cancellationToken)).ConfigureAwait(continueOnCapturedContext: false);
}
catch (OperationCanceledException)
{
// Terminated
}
try
{
await Task.WhenAny(Task.WhenAll(_clientTasks), Task.Delay(Timeout.Infinite, cancellationToken));
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -174,6 +161,7 @@ namespace AMWD.Protocols.Modbus.Tcp
_tcpListener.Dispose(); _tcpListener.Dispose();
_stopCts?.Dispose(); _stopCts?.Dispose();
GC.SuppressFinalize(this);
} }
private void Assertions() private void Assertions()
@@ -196,12 +184,13 @@ namespace AMWD.Protocols.Modbus.Tcp
{ {
try try
{ {
var client = await _tcpListener.AcceptTcpClientAsync(cancellationToken); var client = await _tcpListener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
await _clientListLock.WaitAsync(cancellationToken); await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
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
{ {
@@ -224,23 +213,32 @@ namespace AMWD.Protocols.Modbus.Tcp
{ {
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);
ushort length = headerBytes
.Skip(4).Take(2).ToArray()
.GetBigEndianUInt16();
byte[] bodyBytes = await stream.ReadExpectedBytesAsync(length, 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
@@ -249,7 +247,7 @@ namespace AMWD.Protocols.Modbus.Tcp
} }
finally finally
{ {
await _clientListLock.WaitAsync(cancellationToken); await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
try try
{ {
_clients.Remove(client); _clients.Remove(client);
@@ -324,7 +322,7 @@ namespace AMWD.Protocols.Modbus.Tcp
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++)
@@ -363,7 +361,7 @@ namespace AMWD.Protocols.Modbus.Tcp
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++)
@@ -402,7 +400,7 @@ namespace AMWD.Protocols.Modbus.Tcp
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++)
@@ -436,7 +434,7 @@ namespace AMWD.Protocols.Modbus.Tcp
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++)
@@ -484,7 +482,7 @@ namespace AMWD.Protocols.Modbus.Tcp
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
@@ -524,7 +522,7 @@ namespace AMWD.Protocols.Modbus.Tcp
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
@@ -584,7 +582,7 @@ namespace AMWD.Protocols.Modbus.Tcp
}); });
} }
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
@@ -641,7 +639,7 @@ namespace AMWD.Protocols.Modbus.Tcp
}); });
} }
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
@@ -698,7 +696,7 @@ namespace AMWD.Protocols.Modbus.Tcp
try try
{ {
var deviceInfo = 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>();
@@ -761,7 +759,7 @@ namespace AMWD.Protocols.Modbus.Tcp
} }
} }
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)
@@ -851,5 +849,18 @@ namespace AMWD.Protocols.Modbus.Tcp
} }
#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

@@ -1,15 +1,13 @@
using System.Net; using System.Net;
using System.Net.Sockets;
namespace AMWD.Protocols.Modbus.Tcp.Utils namespace AMWD.Protocols.Modbus.Tcp.Utils
{ {
internal class IPEndPointWrapper /// <inheritdoc cref="IPEndPoint" />
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal class IPEndPointWrapper(EndPoint endPoint)
{ {
private IPEndPoint _ipEndPoint; private readonly IPEndPoint _ipEndPoint = (IPEndPoint)endPoint;
public IPEndPointWrapper(EndPoint endPoint)
{
_ipEndPoint = (IPEndPoint)endPoint;
}
#region Properties #region Properties

View File

@@ -8,14 +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;
public NetworkStreamWrapper(NetworkStream stream)
{
_stream = stream;
}
/// <inheritdoc cref="NetworkStream.Dispose" /> /// <inheritdoc cref="NetworkStream.Dispose" />
public virtual void Dispose() public virtual void Dispose()

View File

@@ -3,14 +3,11 @@ using System.Net.Sockets;
namespace AMWD.Protocols.Modbus.Tcp.Utils namespace AMWD.Protocols.Modbus.Tcp.Utils
{ {
internal class SocketWrapper : IDisposable /// <inheritdoc cref="Socket" />
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal class SocketWrapper(Socket socket) : IDisposable
{ {
private Socket _socket; private readonly Socket _socket = socket;
public SocketWrapper(Socket socket)
{
_socket = socket;
}
/// <inheritdoc cref="Socket.DualMode" /> /// <inheritdoc cref="Socket.DualMode" />
public virtual bool DualMode public virtual bool DualMode

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

@@ -6,22 +6,18 @@ using System.Threading.Tasks;
namespace AMWD.Protocols.Modbus.Tcp.Utils namespace AMWD.Protocols.Modbus.Tcp.Utils
{ {
/// <inheritdoc cref="TcpListener" />
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal class TcpListenerWrapper : IDisposable internal class TcpListenerWrapper(IPAddress localaddr, int port) : IDisposable
{ {
#region Fields #region Fields
private TcpListener _tcpListener; private readonly TcpListener _tcpListener = new(localaddr, port);
#endregion Fields #endregion Fields
#region Constructor #region Constructor
public TcpListenerWrapper(IPAddress localaddr, int port)
{
_tcpListener = new TcpListener(localaddr, port);
}
#endregion Constructor #endregion Constructor
#region Properties #region Properties

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,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup />
<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);

View File

@@ -87,7 +87,7 @@
Assert.IsFalse(success); Assert.IsFalse(success);
} }
[DataTestMethod] [TestMethod]
[DataRow(0xFF)] [DataRow(0xFF)]
[DataRow(0x00)] [DataRow(0x00)]
public void ShouldPrintPrettyString(int highByte) public void ShouldPrintPrettyString(int highByte)

View File

@@ -87,7 +87,7 @@
Assert.IsFalse(success); Assert.IsFalse(success);
} }
[DataTestMethod] [TestMethod]
[DataRow(0xFF)] [DataRow(0xFF)]
[DataRow(0x00)] [DataRow(0x00)]
public void ShouldPrintPrettyString(int highByte) public void ShouldPrintPrettyString(int highByte)

View File

@@ -1,6 +1,5 @@
using System.Collections.Generic; using AMWD.Protocols.Modbus.Common.Models;
using System.Reflection; using System.Reflection;
using AMWD.Protocols.Modbus.Common.Models;
namespace AMWD.Protocols.Modbus.Tests.Common.Models namespace AMWD.Protocols.Modbus.Tests.Common.Models
{ {
@@ -134,7 +133,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Models
.GetField("_coils", BindingFlags.NonPublic | BindingFlags.Instance) .GetField("_coils", BindingFlags.NonPublic | BindingFlags.Instance)
.GetValue(device)).ToArray(); .GetValue(device)).ToArray();
Assert.AreEqual(1, coils.Length); Assert.HasCount(1, coils);
Assert.AreEqual(111, coils.First()); Assert.AreEqual(111, coils.First());
} }
@@ -179,7 +178,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Models
.GetField("_discreteInputs", BindingFlags.NonPublic | BindingFlags.Instance) .GetField("_discreteInputs", BindingFlags.NonPublic | BindingFlags.Instance)
.GetValue(device)).ToArray(); .GetValue(device)).ToArray();
Assert.AreEqual(1, discreteInputs.Length); Assert.HasCount(1, discreteInputs);
Assert.AreEqual(111, discreteInputs.First()); Assert.AreEqual(111, discreteInputs.First());
} }
@@ -229,7 +228,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Models
.GetValue(device)) .GetValue(device))
.ToDictionary(x => x.Key, x => x.Value); .ToDictionary(x => x.Key, x => x.Value);
Assert.AreEqual(1, registers.Count); Assert.HasCount(1, registers);
Assert.AreEqual(111, registers.First().Key); Assert.AreEqual(111, registers.First().Key);
Assert.AreEqual(42, registers.First().Value); Assert.AreEqual(42, registers.First().Value);
} }
@@ -280,7 +279,7 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Models
.GetValue(device)) .GetValue(device))
.ToDictionary(x => x.Key, x => x.Value); .ToDictionary(x => x.Key, x => x.Value);
Assert.AreEqual(1, registers.Count); Assert.HasCount(1, registers);
Assert.AreEqual(111, registers.First().Key); Assert.AreEqual(111, registers.First().Key);
Assert.AreEqual(42, registers.First().Value); Assert.AreEqual(42, registers.First().Value);
} }

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