Compare commits
38 Commits
v0.3.0
...
42af5a06f2
| Author | SHA1 | Date | |
|---|---|---|---|
| 42af5a06f2 | |||
| f7978c6277 | |||
| 56b83f2d33 | |||
| 793a088c5b | |||
| b216304482 | |||
| e87649ff43 | |||
| 11295ea247 | |||
| 799a014b15 | |||
| 885079ae70 | |||
| 17fc216658 | |||
| 885231466b | |||
| 5b8a2a8af1 | |||
| 980dab22f3 | |||
| 9270f49519 | |||
| 241a9d114c | |||
| 9283b04971 | |||
| 8b3441f6dd | |||
| 63c88f5da7 | |||
| e7300bfbde | |||
| 38dd94471d | |||
| 56664cdac5 | |||
| 4ef7500c3b | |||
| 05759f8e12 | |||
| 6fc7cfda9a | |||
| fb67e0b77e | |||
| ce3d873cd0 | |||
| 1cf49f74ea | |||
| 39863880d5 | |||
| ec0ba31b86 | |||
| 96b5ee21c8 | |||
| 6a231e02cb | |||
| c1a70de6bb | |||
| 6bf011d53f | |||
| 0c81ab6b44 | |||
| 3e8f2cd73b | |||
| e830e43c36 | |||
| 6a63dbb739 | |||
| 1536c60336 |
@@ -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
|
||||||
|
|||||||
60
.gitea/workflows/branch-build.yml
Normal file
60
.gitea/workflows/branch-build.yml
Normal 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
|
||||||
78
.gitea/workflows/release-build.yml
Normal file
78
.gitea/workflows/release-build.yml
Normal 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
3
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
124
.gitlab-ci.yml
124
.gitlab-ci.yml
@@ -1,124 +0,0 @@
|
|||||||
image: mcr.microsoft.com/dotnet/sdk:8.0
|
|
||||||
|
|
||||||
variables:
|
|
||||||
TZ: "Europe/Berlin"
|
|
||||||
LANG: "de"
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- build
|
|
||||||
- test
|
|
||||||
- deploy
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
build-debug:
|
|
||||||
stage: build
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
- lnx
|
|
||||||
- 64bit
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG == null
|
|
||||||
script:
|
|
||||||
- dotnet restore --no-cache --force
|
|
||||||
- dotnet build -c Debug --nologo --no-restore --no-incremental
|
|
||||||
- mkdir ./artifacts
|
|
||||||
- mv ./AMWD.Protocols.Modbus.Common/bin/Debug/*.nupkg ./artifacts/
|
|
||||||
- mv ./AMWD.Protocols.Modbus.Common/bin/Debug/*.snupkg ./artifacts/
|
|
||||||
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Debug/*.nupkg ./artifacts/
|
|
||||||
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Debug/*.snupkg ./artifacts/
|
|
||||||
- mv ./AMWD.Protocols.Modbus.Serial/bin/Debug/*.nupkg ./artifacts/
|
|
||||||
- mv ./AMWD.Protocols.Modbus.Serial/bin/Debug/*.snupkg ./artifacts/
|
|
||||||
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Debug/*.nupkg ./artifacts/
|
|
||||||
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Debug/*.snupkg ./artifacts/
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- artifacts/*.nupkg
|
|
||||||
- artifacts/*.snupkg
|
|
||||||
expire_in: 1 days
|
|
||||||
|
|
||||||
test-debug:
|
|
||||||
stage: test
|
|
||||||
dependencies:
|
|
||||||
- build-debug
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
- lnx
|
|
||||||
- 64bit
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG == null
|
|
||||||
coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/'
|
|
||||||
script:
|
|
||||||
- dotnet restore --no-cache --force
|
|
||||||
- dotnet test -c Debug --nologo --no-restore
|
|
||||||
|
|
||||||
deploy-debug:
|
|
||||||
stage: deploy
|
|
||||||
dependencies:
|
|
||||||
- build-debug
|
|
||||||
- test-debug
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
- lnx
|
|
||||||
- 64bit
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG == null
|
|
||||||
script:
|
|
||||||
- dotnet nuget push -k $BAGET_APIKEY -s https://nuget.am-wd.de/v3/index.json --skip-duplicate artifacts/*.nupkg
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
build-release:
|
|
||||||
stage: build
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
- lnx
|
|
||||||
- amd64
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG != null
|
|
||||||
script:
|
|
||||||
- dotnet restore --no-cache --force
|
|
||||||
- dotnet build -c Release --nologo --no-restore --no-incremental
|
|
||||||
- mkdir ./artifacts
|
|
||||||
- mv ./AMWD.Protocols.Modbus.Common/bin/Release/*.nupkg ./artifacts/
|
|
||||||
- mv ./AMWD.Protocols.Modbus.Common/bin/Release/*.snupkg ./artifacts/
|
|
||||||
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Release/*.nupkg ./artifacts/
|
|
||||||
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Release/*.snupkg ./artifacts/
|
|
||||||
- mv ./AMWD.Protocols.Modbus.Serial/bin/Release/*.nupkg ./artifacts/
|
|
||||||
- mv ./AMWD.Protocols.Modbus.Serial/bin/Release/*.snupkg ./artifacts/
|
|
||||||
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Release/*.nupkg ./artifacts/
|
|
||||||
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Release/*.snupkg ./artifacts/
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- artifacts/*.nupkg
|
|
||||||
- artifacts/*.snupkg
|
|
||||||
expire_in: 1 days
|
|
||||||
|
|
||||||
test-release:
|
|
||||||
stage: test
|
|
||||||
dependencies:
|
|
||||||
- build-release
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
- lnx
|
|
||||||
- amd64
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG != null
|
|
||||||
coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/'
|
|
||||||
script:
|
|
||||||
- dotnet restore --no-cache --force
|
|
||||||
- dotnet test -c Release --nologo --no-restore
|
|
||||||
|
|
||||||
deploy-release:
|
|
||||||
stage: deploy
|
|
||||||
dependencies:
|
|
||||||
- build-release
|
|
||||||
- test-release
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
- lnx
|
|
||||||
- 64bit
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG != null
|
|
||||||
script:
|
|
||||||
- dotnet nuget push -k $NUGET_APIKEY -s https://api.nuget.org/v3/index.json --skip-duplicate artifacts/*.nupkg
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("AMWD.Protocols.Modbus.Tests")]
|
|
||||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
|
||||||
<LangVersion>12.0</LangVersion>
|
|
||||||
|
|
||||||
<PackageId>AMWD.Protocols.Modbus.Proxy</PackageId>
|
|
||||||
<AssemblyName>amwd-modbus-proxy</AssemblyName>
|
|
||||||
<RootNamespace>AMWD.Protocols.Modbus.Proxy</RootNamespace>
|
|
||||||
|
|
||||||
<Product>Modbus Proxy Clients</Product>
|
|
||||||
<Description>Plugging Modbus Servers and Clients together to create Modbus Proxies.</Description>
|
|
||||||
<PackageTags>Modbus Protocol Proxy</PackageTags>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" />
|
|
||||||
<Compile Include="../AMWD.Protocols.Modbus.Tcp/Extensions/StreamExtensions.cs" Link="Extensions/StreamExtensions.cs" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="README.md" Pack="true" PackagePath="/" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
|
||||||
<PackageReference Include="System.IO.Ports" Version="4.7.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
|
|
||||||
<PackageReference Include="System.IO.Ports" Version="6.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
|
||||||
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
|
||||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# Modbus Protocol for .NET | Proxy
|
|
||||||
|
|
||||||
With this package the server and client implementations will be combined as proxy.
|
|
||||||
|
|
||||||
You can use any `ModbusBasClient` implementation as target client and plug it into the implemented `ModbusTcpProxy` or `ModbusRtuProxy`, which implement the server side.
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Published under MIT License (see [**tl;dr**Legal](https://www.tldrlegal.com/license/mit-license))
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,26 +0,0 @@
|
|||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace System.IO
|
|
||||||
{
|
|
||||||
internal static class StreamExtensions
|
|
||||||
{
|
|
||||||
public static async Task<byte[]> ReadExpectedBytesAsync(this Stream stream, int expectedBytes, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
byte[] buffer = new byte[expectedBytes];
|
|
||||||
int offset = 0;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken);
|
|
||||||
if (count < 1)
|
|
||||||
throw new EndOfStreamException();
|
|
||||||
|
|
||||||
offset += count;
|
|
||||||
}
|
|
||||||
while (offset < expectedBytes && !cancellationToken.IsCancellationRequested);
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,36 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<LangVersion>12.0</LangVersion>
|
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
<IsTestProject>true</IsTestProject>
|
|
||||||
<CollectCoverage>true</CollectCoverage>
|
|
||||||
|
|
||||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
|
||||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
|
||||||
<PackageReference Include="Moq" Version="4.20.70" />
|
|
||||||
<PackageReference Include="MSTest.TestAdapter" Version="3.2.2" />
|
|
||||||
<PackageReference Include="MSTest.TestFramework" Version="3.2.2" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
|
||||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj" />
|
|
||||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -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;
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
# Visual Studio Version 17
|
|
||||||
VisualStudioVersion = 17.8.34525.116
|
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Common", "AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj", "{2B7689D8-9E56-4DEB-B40E-F70DB4A6F250}"
|
|
||||||
EndProject
|
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0C43172F-63F3-455A-A5FC-CAE7492A969B}"
|
|
||||||
EndProject
|
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{A5A9AEA2-3AFF-4536-9FF9-34663DA4D0AD}"
|
|
||||||
ProjectSection(SolutionItems) = preProject
|
|
||||||
CHANGELOG.md = CHANGELOG.md
|
|
||||||
LICENSE.txt = LICENSE.txt
|
|
||||||
package-icon.png = package-icon.png
|
|
||||||
README.md = README.md
|
|
||||||
EndProjectSection
|
|
||||||
EndProject
|
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "config", "config", "{2ED08B2B-1F72-4E1E-9586-1DC6BEFD7BA7}"
|
|
||||||
ProjectSection(SolutionItems) = preProject
|
|
||||||
.editorconfig = .editorconfig
|
|
||||||
.gitignore = .gitignore
|
|
||||||
CodeMaid.config = CodeMaid.config
|
|
||||||
nuget.config = nuget.config
|
|
||||||
EndProjectSection
|
|
||||||
EndProject
|
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{C8065AE3-BA87-49AC-8100-C85D6DF7E436}"
|
|
||||||
ProjectSection(SolutionItems) = preProject
|
|
||||||
.gitlab-ci.yml = .gitlab-ci.yml
|
|
||||||
Directory.Build.props = Directory.Build.props
|
|
||||||
EndProjectSection
|
|
||||||
EndProject
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Tests", "AMWD.Protocols.Modbus.Tests\AMWD.Protocols.Modbus.Tests.csproj", "{146070C4-E922-4F5A-AD6F-9A899186E26E}"
|
|
||||||
EndProject
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Tcp", "AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj", "{8C888A84-CD09-4087-B5DA-67708ABBABA2}"
|
|
||||||
EndProject
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Serial", "AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj", "{D966826F-EE6C-4BC0-9185-C2A9A50FD586}"
|
|
||||||
EndProject
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Proxy", "AMWD.Protocols.Modbus.Proxy\AMWD.Protocols.Modbus.Proxy.csproj", "{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{2B7689D8-9E56-4DEB-B40E-F70DB4A6F250}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{2B7689D8-9E56-4DEB-B40E-F70DB4A6F250}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{2B7689D8-9E56-4DEB-B40E-F70DB4A6F250}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{2B7689D8-9E56-4DEB-B40E-F70DB4A6F250}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{146070C4-E922-4F5A-AD6F-9A899186E26E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{146070C4-E922-4F5A-AD6F-9A899186E26E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{146070C4-E922-4F5A-AD6F-9A899186E26E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{146070C4-E922-4F5A-AD6F-9A899186E26E}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{8C888A84-CD09-4087-B5DA-67708ABBABA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{8C888A84-CD09-4087-B5DA-67708ABBABA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{8C888A84-CD09-4087-B5DA-67708ABBABA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{8C888A84-CD09-4087-B5DA-67708ABBABA2}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(NestedProjects) = preSolution
|
|
||||||
{A5A9AEA2-3AFF-4536-9FF9-34663DA4D0AD} = {0C43172F-63F3-455A-A5FC-CAE7492A969B}
|
|
||||||
{2ED08B2B-1F72-4E1E-9586-1DC6BEFD7BA7} = {0C43172F-63F3-455A-A5FC-CAE7492A969B}
|
|
||||||
{C8065AE3-BA87-49AC-8100-C85D6DF7E436} = {0C43172F-63F3-455A-A5FC-CAE7492A969B}
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
|
||||||
SolutionGuid = {E4FD8EF0-3594-4994-BE80-5FADA5EE17B4}
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
||||||
37
AMWD.Protocols.Modbus.slnx
Normal file
37
AMWD.Protocols.Modbus.slnx
Normal 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>
|
||||||
BIN
AMWD.Protocols.Modbus.snk
Normal file
BIN
AMWD.Protocols.Modbus.snk
Normal file
Binary file not shown.
88
CHANGELOG.md
88
CHANGELOG.md
@@ -2,33 +2,100 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
_no changes_
|
### Added
|
||||||
|
|
||||||
|
- New automatic documentation generation using docfx.
|
||||||
|
- Additional articles for the documentation.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Reorganized folder structure to allow documentation generation.
|
||||||
|
- Migrated main repository from GitLab to Gitea.
|
||||||
|
- Migrated to VS 2026.
|
||||||
|
- Updated UnitTests and Tools to .NET 10.
|
||||||
|
|
||||||
|
|
||||||
|
## [v0.4.2] (2025-02-07)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixing issue with R/W timeouts while processing client requests on the `ModbusTcpProxy`.
|
||||||
|
|
||||||
|
|
||||||
|
## [v0.4.1] (2025-02-06)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Async methods do not return on captured context anymore (`Task.ConfigureAwait(false)`).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Set `Socket.DualMode` on IPv4 network address is not allowed (`ModbusTcpProxy`).
|
||||||
|
|
||||||
|
|
||||||
|
## [v0.4.0] (2025-01-29)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Small CLI client for Modbus communication.
|
||||||
|
- Small CLI proxy to forward messages.
|
||||||
|
- `VirtualModbusClient` added to `AMWD.Protocols.Modbus.Common`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The `ModbusTcpProxy.ReadWriteTimeout` has a default value of 100 seconds (same default as a `HttpClient` has).
|
||||||
|
- The `ModbusRtuProxy` moved from `AMWD.Protocols.Modbus.Proxy` to `AMWD.Protocols.Modbus.Serial`.
|
||||||
|
- The `ModbusTcpProxy` moved from `AMWD.Protocols.Modbus.Proxy` to `AMWD.Protocols.Modbus.Tcp`.
|
||||||
|
- Server implementations are proxies with a virtual Modbus client.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Discontinue the `AMWD.Protocols.Modbus.Proxy` package (introduced in [v0.3.0]).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Wrong _following bytes_ calculation in `ModbusTcpProxy`.
|
||||||
|
- Wrong processing of `WriteMultipleHoldingRegisters` for proxies.
|
||||||
|
|
||||||
|
|
||||||
|
## [v0.3.2] (2024-09-04)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Build configuration for strong named assemblies.
|
||||||
|
|
||||||
|
|
||||||
|
## [v0.3.1] (2024-06-28)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Issues with range validation on several lines of code in server implementations.
|
||||||
|
|
||||||
|
|
||||||
## [v0.3.0] (2024-05-31)
|
## [v0.3.0] (2024-05-31)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- New `AMWD.Protocols.Modbus.Proxy` package, that contains the server implementations as proxies
|
- New `AMWD.Protocols.Modbus.Proxy` package, that contains the server implementations as proxies.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Renamed `ModbusSerialServer` to `ModbusRtuServer` to clearify the protocol that is used
|
- Renamed `ModbusSerialServer` to `ModbusRtuServer` to clearify the protocol that is used.
|
||||||
- Made `Protocol` property of `ModbusClientBase` non-abstract
|
- Made `Protocol` property of `ModbusClientBase` non-abstract.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Issue with missing client on TCP connection when using default constructor (seems that `AddressFamily.Unknown` caused the problem)
|
- Issue with missing client on TCP connection when using default constructor (seems that `AddressFamily.Unknown` caused the problem).
|
||||||
|
|
||||||
|
|
||||||
## [v0.2.0] (2024-04-02)
|
## [v0.2.0] (2024-04-02)
|
||||||
|
|
||||||
First "final" re-implementation
|
First "final" re-implementation.
|
||||||
|
|
||||||
|
|
||||||
## v0.1.0 (2022-08-28)
|
## v0.1.0 (2022-08-28)
|
||||||
@@ -38,6 +105,11 @@ So this tag is only here for documentation purposes of the NuGet Gallery.
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.0...HEAD
|
[Unreleased]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.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.3.2]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.1...v0.3.2
|
||||||
|
[v0.3.1]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.0...v0.3.1
|
||||||
[v0.3.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.2.0...v0.3.0
|
[v0.3.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.2.0...v0.3.0
|
||||||
[v0.2.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/tree/v0.2.0
|
[v0.2.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/tree/v0.2.0
|
||||||
|
|||||||
@@ -1,50 +1,20 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
<LangVersion>14.0</LangVersion>
|
||||||
<NrtRevisionFormat>{semvertag:main}{!:-dev}</NrtRevisionFormat>
|
<NrtRevisionFormat>{semvertag:main}{!:-dev}</NrtRevisionFormat>
|
||||||
|
|
||||||
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
|
||||||
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
|
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
|
||||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
|
||||||
|
|
||||||
<RepositoryType>git</RepositoryType>
|
|
||||||
<RepositoryUrl>https://github.com/AM-WD/AMWD.Protocols.Modbus.git</RepositoryUrl>
|
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
|
||||||
|
|
||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
|
||||||
<IncludeSymbols>true</IncludeSymbols>
|
|
||||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
|
||||||
<EmbedUntrackedSources>false</EmbedUntrackedSources>
|
|
||||||
|
|
||||||
<PackageIcon>package-icon.png</PackageIcon>
|
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
|
||||||
<PackageProjectUrl>https://wiki.am-wd.de/libs/modbus</PackageProjectUrl>
|
|
||||||
|
|
||||||
<Title>Modbus Protocol for .NET</Title>
|
<Title>Modbus Protocol for .NET</Title>
|
||||||
<Company>AM.WD</Company>
|
<Company>AM.WD</Company>
|
||||||
<Authors>Andreas Müller</Authors>
|
<Authors>Andreas Müller</Authors>
|
||||||
<Copyright>© {copyright:2018-} AM.WD</Copyright>
|
<Copyright>© {copyright:2018-} AM.WD</Copyright>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
|
||||||
|
<SignAssembly>true</SignAssembly>
|
||||||
|
<PublicKey>0024000004800000940000000602000000240000525341310004000001000100adcc4f9f5bb3ac73cb30661f6f35772b8f90a74412925764a960af06ef125bdcec05ed1d139503d5203fb72aa3fa74bab58e82ac2a6cd4b650f8cbf7086a71bc2dfc67e95b8d26d776d60856acf3121f831529b1a4dee91b34ac84f95f71a1165b7783edb591929ba2a684100c92bbed8859c7266fb507f6f55bb6f7fcac80b4</PublicKey>
|
||||||
|
<MoqPublicKey>0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7</MoqPublicKey>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(GITLAB_CI)' == 'true'">
|
|
||||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup Condition="'$(GITLAB_CI)' == 'true'">
|
|
||||||
<SourceLinkGitLabHost Include="$(CI_SERVER_HOST)" Version="$(CI_SERVER_VERSION)" />
|
|
||||||
<PackageReference Include="Microsoft.SourceLink.GitLab" Version="8.0.0">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="../package-icon.png" Pack="true" PackagePath="/" />
|
<PackageReference Include="AMWD.NetRevisionTask" Version="1.4.0">
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Unclassified.NetRevisionTask" Version="0.4.3">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
The MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) Andreas Müller
|
Copyright (c) Andreas Müller
|
||||||
|
|
||||||
@@ -9,8 +9,9 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|||||||
copies of the Software, and to permit persons to whom the Software is
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
furnished to do so, subject to the following conditions:
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
The above copyright notice and this permission notice (including the next
|
||||||
all copies or substantial portions of the Software.
|
paragraph) shall be included in all copies or substantial portions of the
|
||||||
|
Software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The project is divided into four parts.
|
The project is divided into multiple parts.
|
||||||
|
|
||||||
To be mentioned at the beginning:
|
To be mentioned at the beginning:
|
||||||
Only the clients are build very modular to fit any requirement reached on the first implementation back in 2018 ([see here]).
|
Only the clients are build very modular to fit any requirement reached on the first implementation back in 2018 ([see here]).
|
||||||
@@ -20,11 +22,6 @@ For example the default protocol versions: `TCP`, `RTU` and `ASCII`.
|
|||||||
With this package you'll have anything you need to create your own client implementations.
|
With this package you'll have anything you need to create your own client implementations.
|
||||||
|
|
||||||
|
|
||||||
### [Proxy]
|
|
||||||
|
|
||||||
The package contains a TCP and a RTU server implementation as proxy which contains a client of your choice to connect to.
|
|
||||||
|
|
||||||
|
|
||||||
### [Serial]
|
### [Serial]
|
||||||
|
|
||||||
This package contains some wrappers and implementations for the serial protocol.
|
This package contains some wrappers and implementations for the serial protocol.
|
||||||
@@ -39,16 +36,13 @@ It uses a specific TCP connection implementation and plugs all things from the C
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Published under [MIT License] (see [**tl;dr**Legal])
|
Published under [MIT License] (see [choose a license])
|
||||||
[](https://link.am-wd.de/donate)
|
|
||||||
[](https://link.am-wd.de/codeium)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[see here]: https://github.com/andreasAMmueller/Modbus
|
[see here]: https://github.com/andreasAMmueller/Modbus
|
||||||
[Common]: AMWD.Protocols.Modbus.Common/README.md
|
[Common]: src/AMWD.Protocols.Modbus.Common/README.md
|
||||||
[Proxy]: AMWD.Protocols.Modbus.Proxy/README.md
|
[Serial]: src/AMWD.Protocols.Modbus.Serial/README.md
|
||||||
[Serial]: AMWD.Protocols.Modbus.Serial/README.md
|
[TCP]: src/AMWD.Protocols.Modbus.Tcp/README.md
|
||||||
[TCP]: AMWD.Protocols.Modbus.Tcp/README.md
|
|
||||||
[MIT License]: LICENSE.txt
|
[MIT License]: LICENSE.txt
|
||||||
[**tl;dr**Legal]: https://www.tldrlegal.com/license/mit-license
|
[choose a license]: https://choosealicense.com/licenses/mit/
|
||||||
|
|||||||
106
docs/articles/getting-started.md
Normal file
106
docs/articles/getting-started.md
Normal 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
|
||||||
20
docs/articles/introduction.md
Normal file
20
docs/articles/introduction.md
Normal 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
8
docs/articles/toc.yml
Normal 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
63
docs/docfx.json
Normal 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>© AM.WD — 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
BIN
docs/images/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
168
docs/images/logo.svg
Normal file
168
docs/images/logo.svg
Normal 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
26
docs/index.md
Normal 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] |  | Common data for Modbus protocol. |
|
||||||
|
| [AMWD.Protocols.Modbus.Serial] |  | Implementation of the Modbus protocol communicating via serial line using RTU or ASCII encoding. |
|
||||||
|
| [AMWD.Protocols.Modbus.Tcp] |  | 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
3
docs/templates/amwd/public/main.css
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#logo {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
4
docs/toc.yml
Normal file
4
docs/toc.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
- name: API
|
||||||
|
href: api/
|
||||||
|
- name: Articles
|
||||||
|
href: articles/
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
|
||||||
<LangVersion>12.0</LangVersion>
|
|
||||||
|
|
||||||
<PackageId>AMWD.Protocols.Modbus.Common</PackageId>
|
<PackageId>AMWD.Protocols.Modbus.Common</PackageId>
|
||||||
<AssemblyName>amwd-modbus-common</AssemblyName>
|
<AssemblyName>amwd-modbus-common</AssemblyName>
|
||||||
<RootNamespace>AMWD.Protocols.Modbus.Common</RootNamespace>
|
<RootNamespace>AMWD.Protocols.Modbus.Common</RootNamespace>
|
||||||
@@ -13,8 +10,4 @@
|
|||||||
<PackageTags>Modbus Protocol</PackageTags>
|
<PackageTags>Modbus Protocol</PackageTags>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="README.md" Pack="true" PackagePath="/" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
24
src/AMWD.Protocols.Modbus.Common/Contracts/IModbusProxy.cs
Normal file
24
src/AMWD.Protocols.Modbus.Common/Contracts/IModbusProxy.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a Modbus proxy.
|
||||||
|
/// </summary>
|
||||||
|
public interface IModbusProxy : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the proxy.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||||
|
Task StartAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops the proxy.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||||
|
Task StopAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,36 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace AMWD.Protocols.Modbus.Common.Contracts
|
namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base implementation of a Modbus client.
|
/// Base implementation of a Modbus client.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class ModbusClientBase : IDisposable
|
/// <remarks>
|
||||||
|
/// Initializes a new instance of the <see cref="ModbusClientBase"/> class with a specific <see cref="IModbusConnection"/>.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="connection">The <see cref="IModbusConnection"/> responsible for invoking the requests.</param>
|
||||||
|
/// <param name="disposeConnection">
|
||||||
|
/// <see langword="true"/> if the connection should be disposed of by Dispose(),
|
||||||
|
/// <see langword="false"/> otherwise if you inted to reuse the connection.
|
||||||
|
/// </param>
|
||||||
|
public abstract class ModbusClientBase(IModbusConnection connection, bool disposeConnection) : IDisposable
|
||||||
{
|
{
|
||||||
private bool _isDisposed;
|
private bool _isDisposed;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether the connection should be disposed of by <see cref="Dispose()"/>.
|
/// Gets or sets a value indicating whether the connection should be disposed of by <see cref="Dispose()"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly bool disposeConnection;
|
protected readonly bool disposeConnection = disposeConnection;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the <see cref="IModbusConnection"/> responsible for invoking the requests.
|
/// Gets or sets the <see cref="IModbusConnection"/> responsible for invoking the requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly IModbusConnection connection;
|
protected readonly IModbusConnection connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ModbusClientBase"/> class with a specific <see cref="IModbusConnection"/>.
|
/// Initializes a new instance of the <see cref="ModbusClientBase"/> class with a specific <see cref="IModbusConnection"/>.
|
||||||
@@ -32,20 +40,6 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
: this(connection, true)
|
: this(connection, true)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ModbusClientBase"/> class with a specific <see cref="IModbusConnection"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="connection">The <see cref="IModbusConnection"/> responsible for invoking the requests.</param>
|
|
||||||
/// <param name="disposeConnection">
|
|
||||||
/// <see langword="true"/> if the connection should be disposed of by Dispose(),
|
|
||||||
/// <see langword="false"/> otherwise if you inted to reuse the connection.
|
|
||||||
/// </param>
|
|
||||||
public ModbusClientBase(IModbusConnection connection, bool disposeConnection)
|
|
||||||
{
|
|
||||||
this.connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
|
||||||
this.disposeConnection = disposeConnection;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the protocol type to use.
|
/// Gets or sets the protocol type to use.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -67,7 +61,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
Assertions();
|
Assertions();
|
||||||
|
|
||||||
var request = Protocol.SerializeReadCoils(unitId, startAddress, count);
|
var request = Protocol.SerializeReadCoils(unitId, startAddress, count);
|
||||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
Protocol.ValidateResponse(request, response);
|
Protocol.ValidateResponse(request, response);
|
||||||
|
|
||||||
// The protocol processes complete bytes from the response.
|
// The protocol processes complete bytes from the response.
|
||||||
@@ -92,7 +86,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
Assertions();
|
Assertions();
|
||||||
|
|
||||||
var request = Protocol.SerializeReadDiscreteInputs(unitId, startAddress, count);
|
var request = Protocol.SerializeReadDiscreteInputs(unitId, startAddress, count);
|
||||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
Protocol.ValidateResponse(request, response);
|
Protocol.ValidateResponse(request, response);
|
||||||
|
|
||||||
// The protocol processes complete bytes from the response.
|
// The protocol processes complete bytes from the response.
|
||||||
@@ -117,7 +111,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
Assertions();
|
Assertions();
|
||||||
|
|
||||||
var request = Protocol.SerializeReadHoldingRegisters(unitId, startAddress, count);
|
var request = Protocol.SerializeReadHoldingRegisters(unitId, startAddress, count);
|
||||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
Protocol.ValidateResponse(request, response);
|
Protocol.ValidateResponse(request, response);
|
||||||
|
|
||||||
var holdingRegisters = Protocol.DeserializeReadHoldingRegisters(response).ToList();
|
var holdingRegisters = Protocol.DeserializeReadHoldingRegisters(response).ToList();
|
||||||
@@ -140,7 +134,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
Assertions();
|
Assertions();
|
||||||
|
|
||||||
var request = Protocol.SerializeReadInputRegisters(unitId, startAddress, count);
|
var request = Protocol.SerializeReadInputRegisters(unitId, startAddress, count);
|
||||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
Protocol.ValidateResponse(request, response);
|
Protocol.ValidateResponse(request, response);
|
||||||
|
|
||||||
var inputRegisters = Protocol.DeserializeReadInputRegisters(response).ToList();
|
var inputRegisters = Protocol.DeserializeReadInputRegisters(response).ToList();
|
||||||
@@ -184,7 +178,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
do
|
do
|
||||||
{
|
{
|
||||||
var request = Protocol.SerializeReadDeviceIdentification(unitId, category, requestObjectId);
|
var request = Protocol.SerializeReadDeviceIdentification(unitId, category, requestObjectId);
|
||||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
Protocol.ValidateResponse(request, response);
|
Protocol.ValidateResponse(request, response);
|
||||||
|
|
||||||
result = Protocol.DeserializeReadDeviceIdentification(response);
|
result = Protocol.DeserializeReadDeviceIdentification(response);
|
||||||
@@ -247,7 +241,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
Assertions();
|
Assertions();
|
||||||
|
|
||||||
var request = Protocol.SerializeWriteSingleCoil(unitId, coil);
|
var request = Protocol.SerializeWriteSingleCoil(unitId, coil);
|
||||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
Protocol.ValidateResponse(request, response);
|
Protocol.ValidateResponse(request, response);
|
||||||
|
|
||||||
var result = Protocol.DeserializeWriteSingleCoil(response);
|
var result = Protocol.DeserializeWriteSingleCoil(response);
|
||||||
@@ -268,7 +262,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
Assertions();
|
Assertions();
|
||||||
|
|
||||||
var request = Protocol.SerializeWriteSingleHoldingRegister(unitId, register);
|
var request = Protocol.SerializeWriteSingleHoldingRegister(unitId, register);
|
||||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
Protocol.ValidateResponse(request, response);
|
Protocol.ValidateResponse(request, response);
|
||||||
|
|
||||||
var result = Protocol.DeserializeWriteSingleHoldingRegister(response);
|
var result = Protocol.DeserializeWriteSingleHoldingRegister(response);
|
||||||
@@ -289,7 +283,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
Assertions();
|
Assertions();
|
||||||
|
|
||||||
var request = Protocol.SerializeWriteMultipleCoils(unitId, coils);
|
var request = Protocol.SerializeWriteMultipleCoils(unitId, coils);
|
||||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
Protocol.ValidateResponse(request, response);
|
Protocol.ValidateResponse(request, response);
|
||||||
|
|
||||||
var (firstAddress, count) = Protocol.DeserializeWriteMultipleCoils(response);
|
var (firstAddress, count) = Protocol.DeserializeWriteMultipleCoils(response);
|
||||||
@@ -309,7 +303,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
Assertions();
|
Assertions();
|
||||||
|
|
||||||
var request = Protocol.SerializeWriteMultipleHoldingRegisters(unitId, registers);
|
var request = Protocol.SerializeWriteMultipleHoldingRegisters(unitId, registers);
|
||||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
Protocol.ValidateResponse(request, response);
|
Protocol.ValidateResponse(request, response);
|
||||||
|
|
||||||
var (firstAddress, count) = Protocol.DeserializeWriteMultipleHoldingRegisters(response);
|
var (firstAddress, count) = Protocol.DeserializeWriteMultipleHoldingRegisters(response);
|
||||||
@@ -8,19 +8,26 @@ namespace AMWD.Protocols.Modbus.Common.Events
|
|||||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
public class CoilWrittenEventArgs : EventArgs
|
public class CoilWrittenEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
|
internal CoilWrittenEventArgs(byte unitId, ushort address, bool value)
|
||||||
|
{
|
||||||
|
UnitId = unitId;
|
||||||
|
Address = address;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the unit id.
|
/// Gets or sets the unit id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte UnitId { get; set; }
|
public byte UnitId { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the coil address.
|
/// Gets or sets the coil address.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ushort Address { get; set; }
|
public ushort Address { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the coil value.
|
/// Gets or sets the coil value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Value { get; set; }
|
public bool Value { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,29 +8,39 @@ namespace AMWD.Protocols.Modbus.Common.Events
|
|||||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
public class RegisterWrittenEventArgs : EventArgs
|
public class RegisterWrittenEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
|
internal RegisterWrittenEventArgs(byte unitId, ushort address, byte highByte, byte lowByte)
|
||||||
|
{
|
||||||
|
UnitId = unitId;
|
||||||
|
Address = address;
|
||||||
|
HighByte = highByte;
|
||||||
|
LowByte = lowByte;
|
||||||
|
|
||||||
|
Value = new[] { highByte, lowByte }.GetBigEndianUInt16();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the unit id.
|
/// Gets or sets the unit id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte UnitId { get; set; }
|
public byte UnitId { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the address of the register.
|
/// Gets or sets the address of the register.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ushort Address { get; set; }
|
public ushort Address { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the value of the register.
|
/// Gets or sets the value of the register.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ushort Value { get; set; }
|
public ushort Value { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the high byte of the register.
|
/// Gets or sets the high byte of the register.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte HighByte { get; set; }
|
public byte HighByte { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the low byte of the register.
|
/// Gets or sets the low byte of the register.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte LowByte { get; set; }
|
public byte LowByte { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
#if !NET8_0_OR_GREATER
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace AMWD.Protocols.Modbus.Common
|
namespace AMWD.Protocols.Modbus.Common
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace AMWD.Protocols.Modbus.Common
|
namespace AMWD.Protocols.Modbus.Common
|
||||||
@@ -12,14 +13,14 @@ namespace AMWD.Protocols.Modbus.Common
|
|||||||
Array.Reverse(bytes);
|
Array.Reverse(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ushort GetBigEndianUInt16(this byte[] bytes, int offset = 0)
|
public static ushort GetBigEndianUInt16(this IReadOnlyList<byte> bytes, int offset = 0)
|
||||||
{
|
{
|
||||||
byte[] b = bytes.Skip(offset).Take(2).ToArray();
|
byte[] b = bytes.Skip(offset).Take(2).ToArray();
|
||||||
b.SwapBigEndian();
|
b.SwapBigEndian();
|
||||||
return BitConverter.ToUInt16(b, 0);
|
return BitConverter.ToUInt16(b, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] ToBigEndianBytes(this ushort value)
|
public static IReadOnlyList<byte> ToBigEndianBytes(this ushort value)
|
||||||
{
|
{
|
||||||
byte[] b = BitConverter.GetBytes(value);
|
byte[] b = BitConverter.GetBytes(value);
|
||||||
b.SwapBigEndian();
|
b.SwapBigEndian();
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace AMWD.Protocols.Modbus.Common
|
namespace AMWD.Protocols.Modbus.Common
|
||||||
{
|
{
|
||||||
@@ -91,5 +92,23 @@ namespace AMWD.Protocols.Modbus.Common
|
|||||||
/// Gets or sets a value indicating whether individual access (<see cref="ModbusDeviceIdentificationCategory.Individual"/>) is allowed.
|
/// Gets or sets a value indicating whether individual access (<see cref="ModbusDeviceIdentificationCategory.Individual"/>) is allowed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsIndividualAccessAllowed { get; set; }
|
public bool IsIndividualAccessAllowed { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
sb.AppendLine(nameof(DeviceIdentification));
|
||||||
|
sb.AppendLine($" {nameof(VendorName)}: {VendorName}");
|
||||||
|
sb.AppendLine($" {nameof(ProductCode)}: {ProductCode}");
|
||||||
|
sb.AppendLine($" {nameof(MajorMinorRevision)}: {MajorMinorRevision}");
|
||||||
|
sb.AppendLine($" {nameof(VendorUrl)}: {VendorUrl}");
|
||||||
|
sb.AppendLine($" {nameof(ProductName)}: {ProductName}");
|
||||||
|
sb.AppendLine($" {nameof(ModelName)}: {ModelName}");
|
||||||
|
sb.AppendLine($" {nameof(UserApplicationName)}: {UserApplicationName}");
|
||||||
|
sb.AppendLine($" {nameof(IsIndividualAccessAllowed)}: {IsIndividualAccessAllowed}");
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,15 +17,11 @@ namespace AMWD.Protocols.Modbus.Common
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
byte[] blob = [HighByte, LowByte];
|
return new[] { HighByte, LowByte }.GetBigEndianUInt16();
|
||||||
blob.SwapBigEndian();
|
|
||||||
return BitConverter.ToUInt16(blob, 0);
|
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
byte[] blob = BitConverter.GetBytes(value);
|
var blob = value.ToBigEndianBytes();
|
||||||
blob.SwapBigEndian();
|
|
||||||
|
|
||||||
HighByte = blob[0];
|
HighByte = blob[0];
|
||||||
LowByte = blob[1];
|
LowByte = blob[1];
|
||||||
}
|
}
|
||||||
@@ -17,9 +17,7 @@ namespace AMWD.Protocols.Modbus.Common
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
byte[] blob = [HighByte, LowByte];
|
return new[] { HighByte, LowByte }.GetBigEndianUInt16();
|
||||||
blob.SwapBigEndian();
|
|
||||||
return BitConverter.ToUInt16(blob, 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ namespace AMWD.Protocols.Modbus.Common.Models
|
|||||||
/// Initializes a new instance of the <see cref="ModbusDevice"/> class.
|
/// Initializes a new instance of the <see cref="ModbusDevice"/> class.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="id">The <see cref="ModbusDevice"/> ID.</param>
|
/// <param name="id">The <see cref="ModbusDevice"/> ID.</param>
|
||||||
public class ModbusDevice(byte id) : IDisposable
|
internal class ModbusDevice(byte id) : IDisposable
|
||||||
{
|
{
|
||||||
private readonly ReaderWriterLockSlim _rwLockCoils = new();
|
private readonly ReaderWriterLockSlim _rwLockCoils = new();
|
||||||
private readonly ReaderWriterLockSlim _rwLockDiscreteInputs = new();
|
private readonly ReaderWriterLockSlim _rwLockDiscreteInputs = new();
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using System;
|
#if NET6_0_OR_GREATER
|
||||||
|
using System;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace AMWD.Protocols.Modbus.Common
|
namespace AMWD.Protocols.Modbus.Common
|
||||||
{
|
{
|
||||||
@@ -92,11 +92,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadCoils:X2}";
|
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadCoils:X2}";
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
||||||
|
|
||||||
// LRC
|
// LRC
|
||||||
@@ -151,11 +151,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadDiscreteInputs:X2}";
|
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadDiscreteInputs:X2}";
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
||||||
|
|
||||||
// LRC
|
// LRC
|
||||||
@@ -209,11 +209,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadHoldingRegisters:X2}";
|
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadHoldingRegisters:X2}";
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
||||||
|
|
||||||
// LRC
|
// LRC
|
||||||
@@ -264,11 +264,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadInputRegisters:X2}";
|
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadInputRegisters:X2}";
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
||||||
|
|
||||||
// LRC
|
// LRC
|
||||||
@@ -383,7 +383,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteSingleCoil:X2}";
|
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteSingleCoil:X2}";
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = coil.Address.ToBigEndianBytes();
|
var addrBytes = coil.Address.ToBigEndianBytes();
|
||||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||||
|
|
||||||
// Value
|
// Value
|
||||||
@@ -426,7 +426,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteSingleRegister:X2}";
|
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteSingleRegister:X2}";
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = register.Address.ToBigEndianBytes();
|
var addrBytes = register.Address.ToBigEndianBytes();
|
||||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||||
|
|
||||||
// Value
|
// Value
|
||||||
@@ -497,11 +497,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteMultipleCoils:X2}";
|
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteMultipleCoils:X2}";
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||||
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
||||||
|
|
||||||
// Byte count
|
// Byte count
|
||||||
@@ -567,11 +567,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteMultipleRegisters:X2}";
|
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteMultipleRegisters:X2}";
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||||
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
||||||
|
|
||||||
// Byte count
|
// Byte count
|
||||||
@@ -675,6 +675,10 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculate LRC for Modbus ASCII.
|
/// Calculate LRC for Modbus ASCII.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The LRC calculation algorithm is defined in the Modbus serial line specification.
|
||||||
|
/// See <see href="https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf">Modbus over Serial Line v1.02</see>, Appendix B, page 38.
|
||||||
|
/// </remarks>
|
||||||
/// <param name="message">The message chars.</param>
|
/// <param name="message">The message chars.</param>
|
||||||
/// <param name="start">The start index.</param>
|
/// <param name="start">The start index.</param>
|
||||||
/// <param name="length">The number of bytes to calculate.</param>
|
/// <param name="length">The number of bytes to calculate.</param>
|
||||||
@@ -6,11 +6,15 @@ using AMWD.Protocols.Modbus.Common.Contracts;
|
|||||||
namespace AMWD.Protocols.Modbus.Common.Protocols
|
namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implementation of the Modbus RTU over TCP protocol.
|
/// Implementation of the Modbus RTU over Modbus TCP protocol.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The Modbus RTU over Modbus TCP is rarely used.
|
/// The Modbus RTU over Modbus TCP is rarely used.
|
||||||
/// It is a non-standard variant of Modbus TCP that includes the Modbus RTU CRC at the end of the message.
|
/// It is a non-standard variant:
|
||||||
|
/// You can define it as RTU message with an additional TCP header
|
||||||
|
/// or as TCP message with an additional CRC16 checksum at the end (header not included!).
|
||||||
|
/// <br/>
|
||||||
|
/// Definition found on <see href="https://www.fernhillsoftware.com/help/drivers/modbus/modbus-protocol.html">Fernhill Software</see>.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public class RtuOverTcpProtocol : IModbusProtocol
|
public class RtuOverTcpProtocol : IModbusProtocol
|
||||||
{
|
{
|
||||||
@@ -115,12 +119,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.ReadCoils;
|
request[7] = (byte)ModbusFunctionCode.ReadCoils;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -178,12 +182,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
|
request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -241,12 +245,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
|
request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -301,12 +305,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.ReadInputRegisters;
|
request[7] = (byte)ModbusFunctionCode.ReadInputRegisters;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -428,7 +432,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
// Function code
|
// Function code
|
||||||
request[7] = (byte)ModbusFunctionCode.WriteSingleCoil;
|
request[7] = (byte)ModbusFunctionCode.WriteSingleCoil;
|
||||||
|
|
||||||
byte[] addrBytes = coil.Address.ToBigEndianBytes();
|
var addrBytes = coil.Address.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
@@ -475,7 +479,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
// Function code
|
// Function code
|
||||||
request[7] = (byte)ModbusFunctionCode.WriteSingleRegister;
|
request[7] = (byte)ModbusFunctionCode.WriteSingleRegister;
|
||||||
|
|
||||||
byte[] addrBytes = register.Address.ToBigEndianBytes();
|
var addrBytes = register.Address.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
@@ -538,12 +542,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils;
|
request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -618,12 +622,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
|
request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -743,7 +747,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
|
|
||||||
// Transaction id
|
// Transaction id
|
||||||
ushort txId = GetNextTransacitonId();
|
ushort txId = GetNextTransacitonId();
|
||||||
byte[] txBytes = txId.ToBigEndianBytes();
|
var txBytes = txId.ToBigEndianBytes();
|
||||||
header[0] = txBytes[0];
|
header[0] = txBytes[0];
|
||||||
header[1] = txBytes[1];
|
header[1] = txBytes[1];
|
||||||
|
|
||||||
@@ -752,7 +756,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
header[3] = 0x00;
|
header[3] = 0x00;
|
||||||
|
|
||||||
// Number of following bytes
|
// Number of following bytes
|
||||||
byte[] countBytes = ((ushort)followingBytes).ToBigEndianBytes();
|
var countBytes = ((ushort)followingBytes).ToBigEndianBytes();
|
||||||
header[4] = countBytes[0];
|
header[4] = countBytes[0];
|
||||||
header[5] = countBytes[1];
|
header[5] = countBytes[1];
|
||||||
|
|
||||||
@@ -10,6 +10,22 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class RtuProtocol : IModbusProtocol
|
public class RtuProtocol : IModbusProtocol
|
||||||
{
|
{
|
||||||
|
#region Fields
|
||||||
|
|
||||||
|
private static readonly byte[] _readFunctionCodes = [
|
||||||
|
(byte)ModbusFunctionCode.ReadCoils,
|
||||||
|
(byte)ModbusFunctionCode.ReadDiscreteInputs,
|
||||||
|
(byte)ModbusFunctionCode.ReadHoldingRegisters,
|
||||||
|
(byte)ModbusFunctionCode.ReadInputRegisters];
|
||||||
|
|
||||||
|
private static readonly byte[] _writeFunctionCodes = [
|
||||||
|
(byte)ModbusFunctionCode.WriteSingleCoil,
|
||||||
|
(byte)ModbusFunctionCode.WriteSingleRegister,
|
||||||
|
(byte)ModbusFunctionCode.WriteMultipleCoils,
|
||||||
|
(byte)ModbusFunctionCode.WriteMultipleRegisters];
|
||||||
|
|
||||||
|
#endregion Fields
|
||||||
|
|
||||||
#region Constants
|
#region Constants
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -96,12 +112,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[1] = (byte)ModbusFunctionCode.ReadCoils;
|
request[1] = (byte)ModbusFunctionCode.ReadCoils;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[2] = addrBytes[0];
|
request[2] = addrBytes[0];
|
||||||
request[3] = addrBytes[1];
|
request[3] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[4] = countBytes[0];
|
request[4] = countBytes[0];
|
||||||
request[5] = countBytes[1];
|
request[5] = countBytes[1];
|
||||||
|
|
||||||
@@ -156,12 +172,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[1] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
|
request[1] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[2] = addrBytes[0];
|
request[2] = addrBytes[0];
|
||||||
request[3] = addrBytes[1];
|
request[3] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[4] = countBytes[0];
|
request[4] = countBytes[0];
|
||||||
request[5] = countBytes[1];
|
request[5] = countBytes[1];
|
||||||
|
|
||||||
@@ -216,12 +232,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[1] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
|
request[1] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[2] = addrBytes[0];
|
request[2] = addrBytes[0];
|
||||||
request[3] = addrBytes[1];
|
request[3] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[4] = countBytes[0];
|
request[4] = countBytes[0];
|
||||||
request[5] = countBytes[1];
|
request[5] = countBytes[1];
|
||||||
|
|
||||||
@@ -273,12 +289,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[1] = (byte)ModbusFunctionCode.ReadInputRegisters;
|
request[1] = (byte)ModbusFunctionCode.ReadInputRegisters;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[2] = addrBytes[0];
|
request[2] = addrBytes[0];
|
||||||
request[3] = addrBytes[1];
|
request[3] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[4] = countBytes[0];
|
request[4] = countBytes[0];
|
||||||
request[5] = countBytes[1];
|
request[5] = countBytes[1];
|
||||||
|
|
||||||
@@ -394,7 +410,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
// Function code
|
// Function code
|
||||||
request[1] = (byte)ModbusFunctionCode.WriteSingleCoil;
|
request[1] = (byte)ModbusFunctionCode.WriteSingleCoil;
|
||||||
|
|
||||||
byte[] addrBytes = coil.Address.ToBigEndianBytes();
|
var addrBytes = coil.Address.ToBigEndianBytes();
|
||||||
request[2] = addrBytes[0];
|
request[2] = addrBytes[0];
|
||||||
request[3] = addrBytes[1];
|
request[3] = addrBytes[1];
|
||||||
|
|
||||||
@@ -438,7 +454,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
// Function code
|
// Function code
|
||||||
request[1] = (byte)ModbusFunctionCode.WriteSingleRegister;
|
request[1] = (byte)ModbusFunctionCode.WriteSingleRegister;
|
||||||
|
|
||||||
byte[] addrBytes = register.Address.ToBigEndianBytes();
|
var addrBytes = register.Address.ToBigEndianBytes();
|
||||||
request[2] = addrBytes[0];
|
request[2] = addrBytes[0];
|
||||||
request[3] = addrBytes[1];
|
request[3] = addrBytes[1];
|
||||||
|
|
||||||
@@ -495,11 +511,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
|
|
||||||
request[1] = (byte)ModbusFunctionCode.WriteMultipleCoils;
|
request[1] = (byte)ModbusFunctionCode.WriteMultipleCoils;
|
||||||
|
|
||||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||||
request[2] = addrBytes[0];
|
request[2] = addrBytes[0];
|
||||||
request[3] = addrBytes[1];
|
request[3] = addrBytes[1];
|
||||||
|
|
||||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||||
request[4] = countBytes[0];
|
request[4] = countBytes[0];
|
||||||
request[5] = countBytes[1];
|
request[5] = countBytes[1];
|
||||||
|
|
||||||
@@ -565,11 +581,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[0] = unitId;
|
request[0] = unitId;
|
||||||
request[1] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
|
request[1] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
|
||||||
|
|
||||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||||
request[2] = addrBytes[0];
|
request[2] = addrBytes[0];
|
||||||
request[3] = addrBytes[1];
|
request[3] = addrBytes[1];
|
||||||
|
|
||||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||||
request[4] = countBytes[0];
|
request[4] = countBytes[0];
|
||||||
request[5] = countBytes[1];
|
request[5] = countBytes[1];
|
||||||
|
|
||||||
@@ -627,7 +643,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
// - 0x03 Read Holding Registers
|
// - 0x03 Read Holding Registers
|
||||||
// - 0x04 Read Input Registers
|
// - 0x04 Read Input Registers
|
||||||
// do have a "following bytes" at position 3
|
// do have a "following bytes" at position 3
|
||||||
if (new[] { 0x01, 0x02, 0x03, 0x04 }.Contains(responseBytes[1]))
|
if (_readFunctionCodes.Contains(responseBytes[1]))
|
||||||
{
|
{
|
||||||
// Unit ID, Function Code, ByteCount, 2x CRC and length of ByteCount
|
// Unit ID, Function Code, ByteCount, 2x CRC and length of ByteCount
|
||||||
if (responseBytes.Count < 5 + responseBytes[2])
|
if (responseBytes.Count < 5 + responseBytes[2])
|
||||||
@@ -638,7 +654,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
// - 0x06 Write Single Register
|
// - 0x06 Write Single Register
|
||||||
// - 0x0F Write Multiple Coils
|
// - 0x0F Write Multiple Coils
|
||||||
// - 0x10 Write Multiple Registers
|
// - 0x10 Write Multiple Registers
|
||||||
if (new[] { 0x05, 0x06, 0x0F, 0x10 }.Contains(responseBytes[1]))
|
if (_writeFunctionCodes.Contains(responseBytes[1]))
|
||||||
{
|
{
|
||||||
// Write Single => Unit ID, Function code, 2x Address, 2x Value, 2x CRC
|
// Write Single => Unit ID, Function code, 2x Address, 2x Value, 2x CRC
|
||||||
// Write Multi => Unit ID, Function code, 2x Address, 2x QuantityWritten, 2x CRC
|
// Write Multi => Unit ID, Function code, 2x Address, 2x QuantityWritten, 2x CRC
|
||||||
@@ -715,13 +731,13 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
if (isError)
|
if (isError)
|
||||||
throw new ModbusException("Remote Error") { ErrorCode = (ModbusErrorCode)response[2] };
|
throw new ModbusException("Remote Error") { ErrorCode = (ModbusErrorCode)response[2] };
|
||||||
|
|
||||||
if (new[] { 0x01, 0x02, 0x03, 0x04 }.Contains(fnCode))
|
if (_readFunctionCodes.Contains(fnCode))
|
||||||
{
|
{
|
||||||
if (response.Count != 5 + response[2])
|
if (response.Count != 5 + response[2])
|
||||||
throw new ModbusException("Number of following bytes does not match.");
|
throw new ModbusException("Number of following bytes does not match.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (new[] { 0x05, 0x06, 0x0F, 0x10 }.Contains(fnCode))
|
if (_writeFunctionCodes.Contains(fnCode))
|
||||||
{
|
{
|
||||||
if (response.Count != 8)
|
if (response.Count != 8)
|
||||||
throw new ModbusException("Number of bytes does not match.");
|
throw new ModbusException("Number of bytes does not match.");
|
||||||
@@ -733,6 +749,10 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculate CRC16 for Modbus RTU.
|
/// Calculate CRC16 for Modbus RTU.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The CRC 16 calculation algorithm is defined in the Modbus serial line specification.
|
||||||
|
/// See <see href="https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf">Modbus over Serial Line v1.02</see>, Appendix B, page 40.
|
||||||
|
/// </remarks>
|
||||||
/// <param name="bytes">The message bytes.</param>
|
/// <param name="bytes">The message bytes.</param>
|
||||||
/// <param name="start">The start index.</param>
|
/// <param name="start">The start index.</param>
|
||||||
/// <param name="length">The number of bytes to calculate.</param>
|
/// <param name="length">The number of bytes to calculate.</param>
|
||||||
@@ -101,12 +101,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.ReadCoils;
|
request[7] = (byte)ModbusFunctionCode.ReadCoils;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -159,12 +159,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
|
request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -217,12 +217,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
|
request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -272,12 +272,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.ReadInputRegisters;
|
request[7] = (byte)ModbusFunctionCode.ReadInputRegisters;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -389,7 +389,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
// Function code
|
// Function code
|
||||||
request[7] = (byte)ModbusFunctionCode.WriteSingleCoil;
|
request[7] = (byte)ModbusFunctionCode.WriteSingleCoil;
|
||||||
|
|
||||||
byte[] addrBytes = coil.Address.ToBigEndianBytes();
|
var addrBytes = coil.Address.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
@@ -431,7 +431,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
// Function code
|
// Function code
|
||||||
request[7] = (byte)ModbusFunctionCode.WriteSingleRegister;
|
request[7] = (byte)ModbusFunctionCode.WriteSingleRegister;
|
||||||
|
|
||||||
byte[] addrBytes = register.Address.ToBigEndianBytes();
|
var addrBytes = register.Address.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
@@ -489,12 +489,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils;
|
request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -564,12 +564,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
|
request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -678,7 +678,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
|
|
||||||
// Transaction id
|
// Transaction id
|
||||||
ushort txId = GetNextTransacitonId();
|
ushort txId = GetNextTransacitonId();
|
||||||
byte[] txBytes = txId.ToBigEndianBytes();
|
var txBytes = txId.ToBigEndianBytes();
|
||||||
header[0] = txBytes[0];
|
header[0] = txBytes[0];
|
||||||
header[1] = txBytes[1];
|
header[1] = txBytes[1];
|
||||||
|
|
||||||
@@ -687,7 +687,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
header[3] = 0x00;
|
header[3] = 0x00;
|
||||||
|
|
||||||
// Number of following bytes
|
// Number of following bytes
|
||||||
byte[] countBytes = ((ushort)followingBytes).ToBigEndianBytes();
|
var countBytes = ((ushort)followingBytes).ToBigEndianBytes();
|
||||||
header[4] = countBytes[0];
|
header[4] = countBytes[0];
|
||||||
header[5] = countBytes[1];
|
header[5] = countBytes[1];
|
||||||
|
|
||||||
478
src/AMWD.Protocols.Modbus.Common/Protocols/VirtualProtocol.cs
Normal file
478
src/AMWD.Protocols.Modbus.Common/Protocols/VirtualProtocol.cs
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||||
|
using AMWD.Protocols.Modbus.Common.Events;
|
||||||
|
using AMWD.Protocols.Modbus.Common.Models;
|
||||||
|
|
||||||
|
namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||||
|
{
|
||||||
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
|
internal class VirtualProtocol : IModbusProtocol, IDisposable
|
||||||
|
{
|
||||||
|
#region Fields
|
||||||
|
|
||||||
|
private bool _isDisposed;
|
||||||
|
|
||||||
|
private readonly ReaderWriterLockSlim _deviceListLock = new();
|
||||||
|
private readonly Dictionary<byte, ModbusDevice> _devices = [];
|
||||||
|
|
||||||
|
#endregion Fields
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
|
||||||
|
_deviceListLock.Dispose();
|
||||||
|
|
||||||
|
foreach (var device in _devices.Values)
|
||||||
|
device.Dispose();
|
||||||
|
|
||||||
|
_devices.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Events
|
||||||
|
|
||||||
|
public event EventHandler<CoilWrittenEventArgs> CoilWritten;
|
||||||
|
|
||||||
|
public event EventHandler<RegisterWrittenEventArgs> RegisterWritten;
|
||||||
|
|
||||||
|
#endregion Events
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
public string Name => nameof(VirtualProtocol);
|
||||||
|
|
||||||
|
#endregion Properties
|
||||||
|
|
||||||
|
#region Protocol
|
||||||
|
|
||||||
|
public bool CheckResponseComplete(IReadOnlyList<byte> responseBytes) => true;
|
||||||
|
|
||||||
|
public IReadOnlyList<Coil> DeserializeReadCoils(IReadOnlyList<byte> response)
|
||||||
|
{
|
||||||
|
if (!_devices.TryGetValue(response[0], out var device))
|
||||||
|
throw new TimeoutException("Device not found.");
|
||||||
|
|
||||||
|
ushort start = response.GetBigEndianUInt16(1);
|
||||||
|
ushort count = response.GetBigEndianUInt16(3);
|
||||||
|
|
||||||
|
return Enumerable.Range(0, count)
|
||||||
|
.Select(i => device.GetCoil((ushort)(start + i)))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeviceIdentificationRaw DeserializeReadDeviceIdentification(IReadOnlyList<byte> response)
|
||||||
|
{
|
||||||
|
if (!_devices.TryGetValue(response[0], out var _))
|
||||||
|
throw new TimeoutException("Device not found.");
|
||||||
|
|
||||||
|
var result = new DeviceIdentificationRaw
|
||||||
|
{
|
||||||
|
AllowsIndividualAccess = false,
|
||||||
|
MoreRequestsNeeded = false,
|
||||||
|
Objects = []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (response[1] >= 1)
|
||||||
|
{
|
||||||
|
string version = GetType().Assembly
|
||||||
|
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
|
||||||
|
.InformationalVersion;
|
||||||
|
|
||||||
|
result.Objects.Add(0, Encoding.UTF8.GetBytes("AM.WD"));
|
||||||
|
result.Objects.Add(1, Encoding.UTF8.GetBytes("AMWD.Protocols.Modbus"));
|
||||||
|
result.Objects.Add(2, Encoding.UTF8.GetBytes(version));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response[1] >= 2)
|
||||||
|
{
|
||||||
|
result.Objects.Add(3, Encoding.UTF8.GetBytes("https://github.com/AM-WD/AMWD.Protocols.Modbus"));
|
||||||
|
result.Objects.Add(4, Encoding.UTF8.GetBytes("Modbus Protocol for .NET"));
|
||||||
|
result.Objects.Add(5, Encoding.UTF8.GetBytes("Virtual Device"));
|
||||||
|
result.Objects.Add(6, Encoding.UTF8.GetBytes("Virtual Modbus Client"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response[1] >= 3)
|
||||||
|
{
|
||||||
|
for (int i = 128; i < 256; i++)
|
||||||
|
result.Objects.Add((byte)i, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<DiscreteInput> DeserializeReadDiscreteInputs(IReadOnlyList<byte> response)
|
||||||
|
{
|
||||||
|
if (!_devices.TryGetValue(response[0], out var device))
|
||||||
|
throw new TimeoutException("Device not found.");
|
||||||
|
|
||||||
|
ushort start = response.GetBigEndianUInt16(1);
|
||||||
|
ushort count = response.GetBigEndianUInt16(3);
|
||||||
|
|
||||||
|
return Enumerable.Range(0, count)
|
||||||
|
.Select(i => device.GetDiscreteInput((ushort)(start + i)))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<HoldingRegister> DeserializeReadHoldingRegisters(IReadOnlyList<byte> response)
|
||||||
|
{
|
||||||
|
if (!_devices.TryGetValue(response[0], out var device))
|
||||||
|
throw new TimeoutException("Device not found.");
|
||||||
|
|
||||||
|
ushort start = response.GetBigEndianUInt16(1);
|
||||||
|
ushort count = response.GetBigEndianUInt16(3);
|
||||||
|
|
||||||
|
return Enumerable.Range(0, count)
|
||||||
|
.Select(i => device.GetHoldingRegister((ushort)(start + i)))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<InputRegister> DeserializeReadInputRegisters(IReadOnlyList<byte> response)
|
||||||
|
{
|
||||||
|
if (!_devices.TryGetValue(response[0], out var device))
|
||||||
|
throw new TimeoutException("Device not found.");
|
||||||
|
|
||||||
|
ushort start = response.GetBigEndianUInt16(1);
|
||||||
|
ushort count = response.GetBigEndianUInt16(3);
|
||||||
|
|
||||||
|
return Enumerable.Range(0, count)
|
||||||
|
.Select(i => device.GetInputRegister((ushort)(start + i)))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public (ushort FirstAddress, ushort NumberOfCoils) DeserializeWriteMultipleCoils(IReadOnlyList<byte> response)
|
||||||
|
{
|
||||||
|
if (!_devices.TryGetValue(response[0], out var device))
|
||||||
|
throw new TimeoutException("Device not found.");
|
||||||
|
|
||||||
|
ushort start = response.GetBigEndianUInt16(1);
|
||||||
|
ushort count = response.GetBigEndianUInt16(3);
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var coil = new Coil
|
||||||
|
{
|
||||||
|
Address = (ushort)(start + i),
|
||||||
|
HighByte = response[5 + i]
|
||||||
|
};
|
||||||
|
device.SetCoil(coil);
|
||||||
|
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CoilWritten?.Invoke(this, new CoilWrittenEventArgs(
|
||||||
|
unitId: response[0],
|
||||||
|
address: coil.Address,
|
||||||
|
value: coil.Value));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (start, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public (ushort FirstAddress, ushort NumberOfRegisters) DeserializeWriteMultipleHoldingRegisters(IReadOnlyList<byte> response)
|
||||||
|
{
|
||||||
|
if (!_devices.TryGetValue(response[0], out var device))
|
||||||
|
throw new TimeoutException("Device not found.");
|
||||||
|
|
||||||
|
ushort start = response.GetBigEndianUInt16(1);
|
||||||
|
ushort count = response.GetBigEndianUInt16(3);
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var register = new HoldingRegister
|
||||||
|
{
|
||||||
|
Address = (ushort)(start + i),
|
||||||
|
HighByte = response[5 + i * 2],
|
||||||
|
LowByte = response[5 + i * 2 + 1]
|
||||||
|
};
|
||||||
|
device.SetHoldingRegister(register);
|
||||||
|
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RegisterWritten?.Invoke(this, new RegisterWrittenEventArgs(
|
||||||
|
unitId: response[0],
|
||||||
|
address: register.Address,
|
||||||
|
highByte: register.HighByte,
|
||||||
|
lowByte: register.LowByte));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (start, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Coil DeserializeWriteSingleCoil(IReadOnlyList<byte> response)
|
||||||
|
{
|
||||||
|
if (!_devices.TryGetValue(response[0], out var device))
|
||||||
|
throw new TimeoutException("Device not found.");
|
||||||
|
|
||||||
|
var coil = new Coil
|
||||||
|
{
|
||||||
|
Address = response.GetBigEndianUInt16(1),
|
||||||
|
HighByte = response[3]
|
||||||
|
};
|
||||||
|
device.SetCoil(coil);
|
||||||
|
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CoilWritten?.Invoke(this, new CoilWrittenEventArgs(
|
||||||
|
unitId: response[0],
|
||||||
|
address: coil.Address,
|
||||||
|
value: coil.Value));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return coil;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HoldingRegister DeserializeWriteSingleHoldingRegister(IReadOnlyList<byte> response)
|
||||||
|
{
|
||||||
|
if (!_devices.TryGetValue(response[0], out var device))
|
||||||
|
throw new TimeoutException("Device not found.");
|
||||||
|
|
||||||
|
var register = new HoldingRegister
|
||||||
|
{
|
||||||
|
Address = response.GetBigEndianUInt16(1),
|
||||||
|
HighByte = response[3],
|
||||||
|
LowByte = response[4]
|
||||||
|
};
|
||||||
|
device.SetHoldingRegister(register);
|
||||||
|
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RegisterWritten?.Invoke(this, new RegisterWrittenEventArgs(
|
||||||
|
unitId: response[0],
|
||||||
|
address: register.Address,
|
||||||
|
highByte: register.HighByte,
|
||||||
|
lowByte: register.LowByte));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return register;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<byte> SerializeReadCoils(byte unitId, ushort startAddress, ushort count)
|
||||||
|
{
|
||||||
|
return [unitId, .. startAddress.ToBigEndianBytes(), .. count.ToBigEndianBytes()];
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<byte> SerializeReadDeviceIdentification(byte unitId, ModbusDeviceIdentificationCategory category, ModbusDeviceIdentificationObject objectId)
|
||||||
|
{
|
||||||
|
if (!Enum.IsDefined(typeof(ModbusDeviceIdentificationCategory), category))
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(category));
|
||||||
|
|
||||||
|
return [unitId, (byte)category];
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<byte> SerializeReadDiscreteInputs(byte unitId, ushort startAddress, ushort count)
|
||||||
|
{
|
||||||
|
return [unitId, .. startAddress.ToBigEndianBytes(), .. count.ToBigEndianBytes()];
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<byte> SerializeReadHoldingRegisters(byte unitId, ushort startAddress, ushort count)
|
||||||
|
{
|
||||||
|
return [unitId, .. startAddress.ToBigEndianBytes(), .. count.ToBigEndianBytes()];
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<byte> SerializeReadInputRegisters(byte unitId, ushort startAddress, ushort count)
|
||||||
|
{
|
||||||
|
return [unitId, .. startAddress.ToBigEndianBytes(), .. count.ToBigEndianBytes()];
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<byte> SerializeWriteMultipleCoils(byte unitId, IReadOnlyList<Coil> coils)
|
||||||
|
{
|
||||||
|
ushort start = coils.OrderBy(c => c.Address).First().Address;
|
||||||
|
ushort count = (ushort)coils.Count;
|
||||||
|
byte[] values = coils.Select(c => c.HighByte).ToArray();
|
||||||
|
|
||||||
|
return [unitId, .. start.ToBigEndianBytes(), .. count.ToBigEndianBytes(), .. values];
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<byte> SerializeWriteMultipleHoldingRegisters(byte unitId, IReadOnlyList<HoldingRegister> registers)
|
||||||
|
{
|
||||||
|
ushort start = registers.OrderBy(c => c.Address).First().Address;
|
||||||
|
ushort count = (ushort)registers.Count;
|
||||||
|
byte[] values = registers.SelectMany(r => new[] { r.HighByte, r.LowByte }).ToArray();
|
||||||
|
|
||||||
|
return [unitId, .. start.ToBigEndianBytes(), .. count.ToBigEndianBytes(), .. values];
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<byte> SerializeWriteSingleCoil(byte unitId, Coil coil)
|
||||||
|
{
|
||||||
|
return [unitId, .. coil.Address.ToBigEndianBytes(), coil.HighByte];
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<byte> SerializeWriteSingleHoldingRegister(byte unitId, HoldingRegister register)
|
||||||
|
{
|
||||||
|
return [unitId, .. register.Address.ToBigEndianBytes(), register.HighByte, register.LowByte];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ValidateResponse(IReadOnlyList<byte> request, IReadOnlyList<byte> response)
|
||||||
|
{
|
||||||
|
if (!request.SequenceEqual(response))
|
||||||
|
throw new InvalidOperationException("Request and response have to be the same on virtual protocol.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Protocol
|
||||||
|
|
||||||
|
#region Device Handling
|
||||||
|
|
||||||
|
public bool AddDevice(byte unitId)
|
||||||
|
{
|
||||||
|
Assertions();
|
||||||
|
using (_deviceListLock.GetWriteLock())
|
||||||
|
{
|
||||||
|
if (_devices.ContainsKey(unitId))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_devices.Add(unitId, new ModbusDevice(unitId));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveDevice(byte unitId)
|
||||||
|
{
|
||||||
|
Assertions();
|
||||||
|
using (_deviceListLock.GetWriteLock())
|
||||||
|
{
|
||||||
|
if (_devices.ContainsKey(unitId))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return _devices.Remove(unitId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Device Handling
|
||||||
|
|
||||||
|
#region Entity Handling
|
||||||
|
|
||||||
|
public Coil GetCoil(byte unitId, ushort address)
|
||||||
|
{
|
||||||
|
Assertions();
|
||||||
|
using (_deviceListLock.GetReadLock())
|
||||||
|
{
|
||||||
|
return _devices.TryGetValue(unitId, out var device)
|
||||||
|
? device.GetCoil(address)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetCoil(byte unitId, Coil coil)
|
||||||
|
{
|
||||||
|
Assertions();
|
||||||
|
using (_deviceListLock.GetWriteLock())
|
||||||
|
{
|
||||||
|
if (_devices.TryGetValue(unitId, out var device))
|
||||||
|
device.SetCoil(coil);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DiscreteInput GetDiscreteInput(byte unitId, ushort address)
|
||||||
|
{
|
||||||
|
Assertions();
|
||||||
|
using (_deviceListLock.GetReadLock())
|
||||||
|
{
|
||||||
|
return _devices.TryGetValue(unitId, out var device)
|
||||||
|
? device.GetDiscreteInput(address)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetDiscreteInput(byte unitId, DiscreteInput discreteInput)
|
||||||
|
{
|
||||||
|
Assertions();
|
||||||
|
using (_deviceListLock.GetWriteLock())
|
||||||
|
{
|
||||||
|
if (_devices.TryGetValue(unitId, out var device))
|
||||||
|
device.SetDiscreteInput(discreteInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public HoldingRegister GetHoldingRegister(byte unitId, ushort address)
|
||||||
|
{
|
||||||
|
Assertions();
|
||||||
|
using (_deviceListLock.GetReadLock())
|
||||||
|
{
|
||||||
|
return _devices.TryGetValue(unitId, out var device)
|
||||||
|
? device.GetHoldingRegister(address)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetHoldingRegister(byte unitId, HoldingRegister holdingRegister)
|
||||||
|
{
|
||||||
|
Assertions();
|
||||||
|
using (_deviceListLock.GetWriteLock())
|
||||||
|
{
|
||||||
|
if (_devices.TryGetValue(unitId, out var device))
|
||||||
|
device.SetHoldingRegister(holdingRegister);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputRegister GetInputRegister(byte unitId, ushort address)
|
||||||
|
{
|
||||||
|
Assertions();
|
||||||
|
using (_deviceListLock.GetReadLock())
|
||||||
|
{
|
||||||
|
return _devices.TryGetValue(unitId, out var device)
|
||||||
|
? device.GetInputRegister(address)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetInputRegister(byte unitId, InputRegister inputRegister)
|
||||||
|
{
|
||||||
|
Assertions();
|
||||||
|
using (_deviceListLock.GetWriteLock())
|
||||||
|
{
|
||||||
|
if (_devices.TryGetValue(unitId, out var device))
|
||||||
|
device.SetInputRegister(inputRegister);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Entity Handling
|
||||||
|
|
||||||
|
private void Assertions()
|
||||||
|
{
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
ObjectDisposedException.ThrowIf(_isDisposed, this);
|
||||||
|
#else
|
||||||
|
if (_isDisposed)
|
||||||
|
throw new ObjectDisposedException(GetType().Name);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,7 +50,8 @@ The different types handled by the Modbus Protocol.
|
|||||||
In addition, you'll find the `DeviceIdentification` there.
|
In addition, you'll find the `DeviceIdentification` there.
|
||||||
It is used for a "special" function called _Read Device Identification_ (0x2B / 43), not supported on all devices.
|
It is used for a "special" function called _Read Device Identification_ (0x2B / 43), not supported on all devices.
|
||||||
|
|
||||||
The `ModbusDevice` is used for the server implementations in the derived packages.
|
The `ModbusDevice` is used for the `VirtualModbusClient`.
|
||||||
|
In combination with the *Proxy implementations (in the derived packages) it can be used as server.
|
||||||
|
|
||||||
|
|
||||||
### Protocols
|
### Protocols
|
||||||
@@ -59,8 +60,8 @@ Here you have the specific default implementations for the Modbus Protocol.
|
|||||||
|
|
||||||
- ASCII
|
- ASCII
|
||||||
- RTU
|
- RTU
|
||||||
- RTU over TCP
|
|
||||||
- TCP
|
- TCP
|
||||||
|
- [RTU over TCP]
|
||||||
|
|
||||||
**NOTE:**
|
**NOTE:**
|
||||||
The implementations over serial line (RTU and ASCII) have a minimum unit ID of one (1) and maximum unit ID of 247 referring to the specification.
|
The implementations over serial line (RTU and ASCII) have a minimum unit ID of one (1) and maximum unit ID of 247 referring to the specification.
|
||||||
@@ -68,4 +69,10 @@ This validation is _not_ implemented here due to real world experience, that som
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Published under MIT License (see [**tl;dr**Legal](https://www.tldrlegal.com/license/mit-license))
|
Published under MIT License (see [choose a license])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[RTU over TCP]: https://www.fernhillsoftware.com/help/drivers/modbus/modbus-protocol.html
|
||||||
|
[choose a license]: https://choosealicense.com/licenses/mit/
|
||||||
|
|
||||||
180
src/AMWD.Protocols.Modbus.Common/Utils/VirtualModbusClient.cs
Normal file
180
src/AMWD.Protocols.Modbus.Common/Utils/VirtualModbusClient.cs
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Threading;
|
||||||
|
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||||
|
using AMWD.Protocols.Modbus.Common.Events;
|
||||||
|
using AMWD.Protocols.Modbus.Common.Models;
|
||||||
|
using AMWD.Protocols.Modbus.Common.Protocols;
|
||||||
|
|
||||||
|
namespace AMWD.Protocols.Modbus.Common.Utils
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implements a virtual Modbus client.
|
||||||
|
/// </summary>
|
||||||
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
|
public class VirtualModbusClient : ModbusClientBase
|
||||||
|
{
|
||||||
|
#region Constructor
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="VirtualModbusClient"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks><strong>DO NOT MODIFY</strong> connection or protocol.</remarks>
|
||||||
|
public VirtualModbusClient()
|
||||||
|
: base(new VirtualConnection())
|
||||||
|
{
|
||||||
|
Protocol = new VirtualProtocol();
|
||||||
|
|
||||||
|
TypedProtocol.CoilWritten += (sender, e) => CoilWritten?.Invoke(this, e);
|
||||||
|
TypedProtocol.RegisterWritten += (sender, e) => RegisterWritten?.Invoke(this, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Constructor
|
||||||
|
|
||||||
|
#region Events
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that a <see cref="Coil"/>-value received through a remote client has been written.
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<CoilWrittenEventArgs> CoilWritten;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that a <see cref="HoldingRegister"/>-value received from a remote client has been written.
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<RegisterWrittenEventArgs> RegisterWritten;
|
||||||
|
|
||||||
|
#endregion Events
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
internal VirtualProtocol TypedProtocol
|
||||||
|
=> Protocol as VirtualProtocol;
|
||||||
|
|
||||||
|
#endregion Properties
|
||||||
|
|
||||||
|
#region Device Handling
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a device to the virtual client.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="unitId">The unit id of the device.</param>
|
||||||
|
/// <returns><see langword="true"/> if the device was added successfully, <see langword="false"/> otherwise.</returns>
|
||||||
|
public bool AddDevice(byte unitId)
|
||||||
|
=> TypedProtocol.AddDevice(unitId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a device from the virtual client.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="unitId">The unit id of the device.</param>
|
||||||
|
/// <returns><see langword="true"/> if the device was removed successfully, <see langword="false"/> otherwise.</returns>
|
||||||
|
public bool RemoveDevice(byte unitId)
|
||||||
|
=> TypedProtocol.RemoveDevice(unitId);
|
||||||
|
|
||||||
|
#endregion Device Handling
|
||||||
|
|
||||||
|
#region Entity Handling
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a <see cref="Coil"/> from the specified <see cref="ModbusDevice"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="unitId">The unit ID of the device.</param>
|
||||||
|
/// <param name="address">The address of the <see cref="Coil"/>.</param>
|
||||||
|
public Coil GetCoil(byte unitId, ushort address)
|
||||||
|
=> TypedProtocol.GetCoil(unitId, address);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a <see cref="Coil"/> to the specified <see cref="ModbusDevice"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="unitId">The unit ID of the device.</param>
|
||||||
|
/// <param name="coil">The <see cref="Coil"/> to set.</param>
|
||||||
|
public void SetCoil(byte unitId, Coil coil)
|
||||||
|
=> TypedProtocol.SetCoil(unitId, coil);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a <see cref="DiscreteInput"/> from the specified <see cref="ModbusDevice"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="unitId">The unit ID of the device.</param>
|
||||||
|
/// <param name="address">The address of the <see cref="DiscreteInput"/>.</param>
|
||||||
|
public DiscreteInput GetDiscreteInput(byte unitId, ushort address)
|
||||||
|
=> TypedProtocol.GetDiscreteInput(unitId, address);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a <see cref="DiscreteInput"/> to the specified <see cref="ModbusDevice"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="unitId">The unit ID of the device.</param>
|
||||||
|
/// <param name="discreteInput">The <see cref="DiscreteInput"/> to set.</param>
|
||||||
|
public void SetDiscreteInput(byte unitId, DiscreteInput discreteInput)
|
||||||
|
=> TypedProtocol.SetDiscreteInput(unitId, discreteInput);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a <see cref="HoldingRegister"/> from the specified <see cref="ModbusDevice"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="unitId">The unit ID of the device.</param>
|
||||||
|
/// <param name="address">The address of the <see cref="HoldingRegister"/>.</param>
|
||||||
|
public HoldingRegister GetHoldingRegister(byte unitId, ushort address)
|
||||||
|
=> TypedProtocol.GetHoldingRegister(unitId, address);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a <see cref="HoldingRegister"/> to the specified <see cref="ModbusDevice"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="unitId">The unit ID of the device.</param>
|
||||||
|
/// <param name="holdingRegister">The <see cref="HoldingRegister"/> to set.</param>
|
||||||
|
public void SetHoldingRegister(byte unitId, HoldingRegister holdingRegister)
|
||||||
|
=> TypedProtocol.SetHoldingRegister(unitId, holdingRegister);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a <see cref="InputRegister"/> from the specified <see cref="ModbusDevice"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="unitId">The unit ID of the device.</param>
|
||||||
|
/// <param name="address">The address of the <see cref="InputRegister"/>.</param>
|
||||||
|
public InputRegister GetInputRegister(byte unitId, ushort address)
|
||||||
|
=> TypedProtocol.GetInputRegister(unitId, address);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a <see cref="InputRegister"/> to the specified <see cref="ModbusDevice"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="unitId">The unit ID of the device.</param>
|
||||||
|
/// <param name="inputRegister">The <see cref="InputRegister"/> to set.</param>
|
||||||
|
public void SetInputRegister(byte unitId, InputRegister inputRegister)
|
||||||
|
=> TypedProtocol.SetInputRegister(unitId, inputRegister);
|
||||||
|
|
||||||
|
#endregion Entity Handling
|
||||||
|
|
||||||
|
#region Methods
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
TypedProtocol.Dispose();
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Methods
|
||||||
|
|
||||||
|
#region Connection
|
||||||
|
|
||||||
|
internal class VirtualConnection : IModbusConnection
|
||||||
|
{
|
||||||
|
public string Name => nameof(VirtualConnection);
|
||||||
|
|
||||||
|
public TimeSpan IdleTimeout { get; set; }
|
||||||
|
|
||||||
|
public TimeSpan ConnectTimeout { get; set; }
|
||||||
|
|
||||||
|
public TimeSpan ReadTimeout { get; set; }
|
||||||
|
|
||||||
|
public TimeSpan WriteTimeout { get; set; }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{ /* nothing to do */ }
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<byte>> InvokeAsync(
|
||||||
|
IReadOnlyList<byte> request,
|
||||||
|
Func<IReadOnlyList<byte>, bool> validateResponseComplete,
|
||||||
|
CancellationToken cancellationToken = default) => Task.FromResult(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Connection
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
<TargetFrameworks>netstandard2.0;net6.0;net8.0;net10.0</TargetFrameworks>
|
||||||
<LangVersion>12.0</LangVersion>
|
|
||||||
|
|
||||||
<PackageId>AMWD.Protocols.Modbus.Serial</PackageId>
|
<PackageId>AMWD.Protocols.Modbus.Serial</PackageId>
|
||||||
<AssemblyName>amwd-modbus-serial</AssemblyName>
|
<AssemblyName>amwd-modbus-serial</AssemblyName>
|
||||||
<RootNamespace>AMWD.Protocols.Modbus.Serial</RootNamespace>
|
<RootNamespace>AMWD.Protocols.Modbus.Serial</RootNamespace>
|
||||||
@@ -14,17 +12,12 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="../AMWD.Protocols.Modbus.Common/InternalsVisibleTo.cs" Link="InternalsVisibleTo.cs" />
|
|
||||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" />
|
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" />
|
||||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ReaderWriterLockSlimExtensions.cs" Link="Extensions/ReaderWriterLockSlimExtensions.cs" />
|
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ReaderWriterLockSlimExtensions.cs" Link="Extensions/ReaderWriterLockSlimExtensions.cs" />
|
||||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/AsyncQueue.cs" Link="Utils/AsyncQueue.cs" />
|
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/AsyncQueue.cs" Link="Utils/AsyncQueue.cs" />
|
||||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/RequestQueueItem.cs" Link="Utils/RequestQueueItem.cs" />
|
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/RequestQueueItem.cs" Link="Utils/RequestQueueItem.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="README.md" Pack="true" PackagePath="/" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||||
<PackageReference Include="System.IO.Ports" Version="4.7.0" />
|
<PackageReference Include="System.IO.Ports" Version="4.7.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -37,12 +30,12 @@
|
|||||||
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
|
||||||
|
<PackageReference Include="System.IO.Ports" Version="10.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Extensions\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -8,20 +8,20 @@ using System.Threading.Tasks;
|
|||||||
using AMWD.Protocols.Modbus.Common;
|
using AMWD.Protocols.Modbus.Common;
|
||||||
using AMWD.Protocols.Modbus.Common.Contracts;
|
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||||
using AMWD.Protocols.Modbus.Common.Protocols;
|
using AMWD.Protocols.Modbus.Common.Protocols;
|
||||||
using AMWD.Protocols.Modbus.Serial;
|
using AMWD.Protocols.Modbus.Serial.Utils;
|
||||||
|
|
||||||
namespace AMWD.Protocols.Modbus.Proxy
|
namespace AMWD.Protocols.Modbus.Serial
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implements a Modbus serial line RTU server proxying all requests to a Modbus client of choice.
|
/// Implements a Modbus serial line RTU server proxying all requests to a Modbus client of choice.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ModbusRtuProxy : IDisposable
|
public class ModbusRtuProxy : IModbusProxy
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
|
|
||||||
private bool _isDisposed;
|
private bool _isDisposed;
|
||||||
|
|
||||||
private readonly SerialPort _serialPort;
|
private readonly SerialPortWrapper _serialPort;
|
||||||
private CancellationTokenSource _stopCts;
|
private CancellationTokenSource _stopCts;
|
||||||
|
|
||||||
#endregion Fields
|
#endregion Fields
|
||||||
@@ -33,31 +33,25 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="client">The <see cref="ModbusClientBase"/> used to request the remote device, that should be proxied.</param>
|
/// <param name="client">The <see cref="ModbusClientBase"/> used to request the remote device, that should be proxied.</param>
|
||||||
/// <param name="portName">The name of the serial port to use.</param>
|
/// <param name="portName">The name of the serial port to use.</param>
|
||||||
/// <param name="baudRate">The baud rate of the serial port (Default: 19.200).</param>
|
public ModbusRtuProxy(ModbusClientBase client, string portName)
|
||||||
public ModbusRtuProxy(ModbusClientBase client, string portName, BaudRate baudRate = BaudRate.Baud19200)
|
|
||||||
{
|
{
|
||||||
Client = client ?? throw new ArgumentNullException(nameof(client));
|
Client = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(portName))
|
if (string.IsNullOrWhiteSpace(portName))
|
||||||
throw new ArgumentNullException(nameof(portName));
|
throw new ArgumentNullException(nameof(portName));
|
||||||
|
|
||||||
if (!Enum.IsDefined(typeof(BaudRate), baudRate))
|
_serialPort = new SerialPortWrapper
|
||||||
throw new ArgumentOutOfRangeException(nameof(baudRate));
|
|
||||||
|
|
||||||
if (!ModbusSerialClient.AvailablePortNames.Contains(portName))
|
|
||||||
throw new ArgumentException($"The serial port ({portName}) is not available.", nameof(portName));
|
|
||||||
|
|
||||||
_serialPort = new SerialPort
|
|
||||||
{
|
{
|
||||||
PortName = portName,
|
PortName = portName,
|
||||||
BaudRate = (int)baudRate,
|
|
||||||
Handshake = Handshake.None,
|
BaudRate = (int)BaudRate.Baud19200,
|
||||||
DataBits = 8,
|
DataBits = 8,
|
||||||
ReadTimeout = 1000,
|
|
||||||
RtsEnable = false,
|
|
||||||
StopBits = StopBits.One,
|
StopBits = StopBits.One,
|
||||||
|
Parity = Parity.Even,
|
||||||
|
Handshake = Handshake.None,
|
||||||
|
ReadTimeout = 1000,
|
||||||
WriteTimeout = 1000,
|
WriteTimeout = 1000,
|
||||||
Parity = Parity.Even
|
RtsEnable = false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,80 +64,108 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ModbusClientBase Client { get; }
|
public ModbusClientBase Client { get; }
|
||||||
|
|
||||||
/// <inheritdoc cref="SerialPort.PortName"/>
|
#region SerialPort Properties
|
||||||
public string PortName => _serialPort.PortName;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc cref="SerialPort.PortName" />
|
||||||
/// Gets or sets the baud rate of the serial port.
|
public virtual string PortName
|
||||||
/// </summary>
|
{
|
||||||
public BaudRate BaudRate
|
get => _serialPort.PortName;
|
||||||
|
set => _serialPort.PortName = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.BaudRate" />
|
||||||
|
public virtual BaudRate BaudRate
|
||||||
{
|
{
|
||||||
get => (BaudRate)_serialPort.BaudRate;
|
get => (BaudRate)_serialPort.BaudRate;
|
||||||
set => _serialPort.BaudRate = (int)value;
|
set => _serialPort.BaudRate = (int)value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="SerialPort.Handshake"/>
|
|
||||||
public Handshake Handshake
|
|
||||||
{
|
|
||||||
get => _serialPort.Handshake;
|
|
||||||
set => _serialPort.Handshake = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="SerialPort.DataBits" />
|
/// <inheritdoc cref="SerialPort.DataBits" />
|
||||||
public int DataBits
|
/// <remarks>
|
||||||
|
/// From the Specs:
|
||||||
|
/// <br/>
|
||||||
|
/// On <see cref="AsciiProtocol"/> it can be 7 or 8.
|
||||||
|
/// <br/>
|
||||||
|
/// On <see cref="RtuProtocol"/> it has to be 8.
|
||||||
|
/// </remarks>
|
||||||
|
public virtual int DataBits
|
||||||
{
|
{
|
||||||
get => _serialPort.DataBits;
|
get => _serialPort.DataBits;
|
||||||
set => _serialPort.DataBits = value;
|
set => _serialPort.DataBits = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="SerialPort.IsOpen"/>
|
/// <inheritdoc cref="SerialPort.Handshake" />
|
||||||
public bool IsOpen => _serialPort.IsOpen;
|
public virtual Handshake Handshake
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a read operation does not finish.
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan ReadTimeout
|
|
||||||
{
|
{
|
||||||
get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout);
|
get => _serialPort.Handshake;
|
||||||
set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds;
|
set => _serialPort.Handshake = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.Parity" />
|
||||||
|
/// <remarks>
|
||||||
|
/// From the Specs:
|
||||||
|
/// <br/>
|
||||||
|
/// <see cref="Parity.Even"/> is recommended and therefore the default value.
|
||||||
|
/// <br/>
|
||||||
|
/// If you use <see cref="Parity.None"/>, <see cref="StopBits.Two"/> is required,
|
||||||
|
/// otherwise <see cref="StopBits.One"/> should work fine.
|
||||||
|
/// </remarks>
|
||||||
|
public virtual Parity Parity
|
||||||
|
{
|
||||||
|
get => _serialPort.Parity;
|
||||||
|
set => _serialPort.Parity = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="SerialPort.RtsEnable" />
|
/// <inheritdoc cref="SerialPort.RtsEnable" />
|
||||||
public bool RtsEnable
|
public virtual bool RtsEnable
|
||||||
{
|
{
|
||||||
get => _serialPort.RtsEnable;
|
get => _serialPort.RtsEnable;
|
||||||
set => _serialPort.RtsEnable = value;
|
set => _serialPort.RtsEnable = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="SerialPort.StopBits" />
|
/// <inheritdoc cref="SerialPort.StopBits" />
|
||||||
public StopBits StopBits
|
/// <remarks>
|
||||||
|
/// From the Specs:
|
||||||
|
/// <br/>
|
||||||
|
/// Should be <see cref="StopBits.One"/> for <see cref="Parity.Even"/> or <see cref="Parity.Odd"/>.
|
||||||
|
/// <br/>
|
||||||
|
/// Should be <see cref="StopBits.Two"/> for <see cref="Parity.None"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public virtual StopBits StopBits
|
||||||
{
|
{
|
||||||
get => _serialPort.StopBits;
|
get => _serialPort.StopBits;
|
||||||
set => _serialPort.StopBits = value;
|
set => _serialPort.StopBits = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPortWrapper.IsOpen"/>
|
||||||
|
public bool IsOpen => _serialPort.IsOpen;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a write operation does not finish.
|
/// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a read/receive operation does not finish.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan WriteTimeout
|
public virtual TimeSpan ReadTimeout
|
||||||
|
{
|
||||||
|
get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout);
|
||||||
|
set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a write/send operation does not finish.
|
||||||
|
/// </summary>
|
||||||
|
public virtual TimeSpan WriteTimeout
|
||||||
{
|
{
|
||||||
get => TimeSpan.FromMilliseconds(_serialPort.WriteTimeout);
|
get => TimeSpan.FromMilliseconds(_serialPort.WriteTimeout);
|
||||||
set => _serialPort.WriteTimeout = (int)value.TotalMilliseconds;
|
set => _serialPort.WriteTimeout = (int)value.TotalMilliseconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="SerialPort.Parity"/>
|
#endregion SerialPort Properties
|
||||||
public Parity Parity
|
|
||||||
{
|
|
||||||
get => _serialPort.Parity;
|
|
||||||
set => _serialPort.Parity = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Properties
|
#endregion Properties
|
||||||
|
|
||||||
#region Control Methods
|
#region Control Methods
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts the server.
|
/// Starts the proxy.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||||
public Task StartAsync(CancellationToken cancellationToken = default)
|
public Task StartAsync(CancellationToken cancellationToken = default)
|
||||||
@@ -164,23 +186,22 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops the server.
|
/// Stops the proxy.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||||
public Task StopAsync(CancellationToken cancellationToken = default)
|
public Task StopAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Assertions();
|
Assertions();
|
||||||
return StopAsyncInternal(cancellationToken);
|
StopAsyncInternal();
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task StopAsyncInternal(CancellationToken cancellationToken)
|
private void StopAsyncInternal()
|
||||||
{
|
{
|
||||||
_stopCts.Cancel();
|
_stopCts?.Cancel();
|
||||||
|
|
||||||
_serialPort.Close();
|
_serialPort.Close();
|
||||||
_serialPort.DataReceived -= OnDataReceived;
|
_serialPort.DataReceived -= OnDataReceived;
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -193,7 +214,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
_isDisposed = true;
|
_isDisposed = true;
|
||||||
|
|
||||||
StopAsyncInternal(CancellationToken.None).Wait();
|
StopAsyncInternal();
|
||||||
|
|
||||||
_serialPort.Dispose();
|
_serialPort.Dispose();
|
||||||
_stopCts?.Dispose();
|
_stopCts?.Dispose();
|
||||||
@@ -207,13 +228,16 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
if (_isDisposed)
|
if (_isDisposed)
|
||||||
throw new ObjectDisposedException(GetType().FullName);
|
throw new ObjectDisposedException(GetType().FullName);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(PortName))
|
||||||
|
throw new ArgumentNullException(nameof(PortName), "The serial port name cannot be empty.");
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Control Methods
|
#endregion Control Methods
|
||||||
|
|
||||||
#region Client Handling
|
#region Client Handling
|
||||||
|
|
||||||
private void OnDataReceived(object _, SerialDataReceivedEventArgs evArgs)
|
private void OnDataReceived(object _, SerialDataReceivedEventArgs __)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -282,16 +306,14 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
default: // unknown function
|
default: // unknown function
|
||||||
{
|
{
|
||||||
byte[] responseBytes = new byte[5];
|
var responseBytes = new List<byte>();
|
||||||
Array.Copy(requestBytes, 0, responseBytes, 0, 2);
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
|
||||||
|
|
||||||
// Mark as error
|
// Mark as error
|
||||||
responseBytes[1] |= 0x80;
|
responseBytes[1] |= 0x80;
|
||||||
|
|
||||||
responseBytes[2] = (byte)ModbusErrorCode.IllegalFunction;
|
return ReturnResponse(responseBytes);
|
||||||
|
|
||||||
SetCrc(responseBytes);
|
|
||||||
return responseBytes;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,7 +331,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.AddRange(requestBytes.Take(2));
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken);
|
var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
|
||||||
byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)];
|
byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)];
|
||||||
for (int i = 0; i < coils.Count; i++)
|
for (int i = 0; i < coils.Count; i++)
|
||||||
@@ -332,8 +354,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
AddCrc(responseBytes);
|
return ReturnResponse(responseBytes);
|
||||||
return [.. responseBytes];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<byte[]> HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
private async Task<byte[]> HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
@@ -349,7 +370,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.AddRange(requestBytes.Take(2));
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken);
|
var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
|
||||||
byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)];
|
byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)];
|
||||||
for (int i = 0; i < discreteInputs.Count; i++)
|
for (int i = 0; i < discreteInputs.Count; i++)
|
||||||
@@ -372,8 +393,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
AddCrc(responseBytes);
|
return ReturnResponse(responseBytes);
|
||||||
return [.. responseBytes];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<byte[]> HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
private async Task<byte[]> HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
@@ -389,7 +409,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.AddRange(requestBytes.Take(2));
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken);
|
var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
|
||||||
byte[] values = new byte[holdingRegisters.Count * 2];
|
byte[] values = new byte[holdingRegisters.Count * 2];
|
||||||
for (int i = 0; i < holdingRegisters.Count; i++)
|
for (int i = 0; i < holdingRegisters.Count; i++)
|
||||||
@@ -407,8 +427,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
AddCrc(responseBytes);
|
return ReturnResponse(responseBytes);
|
||||||
return [.. responseBytes];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<byte[]> HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
private async Task<byte[]> HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
@@ -424,7 +443,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.AddRange(requestBytes.Take(2));
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken);
|
var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
|
||||||
byte[] values = new byte[count * 2];
|
byte[] values = new byte[count * 2];
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
@@ -442,8 +461,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
AddCrc(responseBytes);
|
return ReturnResponse(responseBytes);
|
||||||
return [.. responseBytes];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
private async Task<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
@@ -461,8 +479,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes[1] |= 0x80;
|
responseBytes[1] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
|
||||||
AddCrc(responseBytes);
|
return ReturnResponse(responseBytes);
|
||||||
return [.. responseBytes];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -474,7 +491,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
LowByte = requestBytes[5],
|
LowByte = requestBytes[5],
|
||||||
};
|
};
|
||||||
|
|
||||||
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[0], coil, cancellationToken);
|
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[0], coil, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
if (isSuccess)
|
if (isSuccess)
|
||||||
{
|
{
|
||||||
// Response is an echo of the request
|
// Response is an echo of the request
|
||||||
@@ -492,8 +509,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
AddCrc(responseBytes);
|
return ReturnResponse(responseBytes);
|
||||||
return [.. responseBytes];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
private async Task<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
@@ -514,7 +530,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
LowByte = requestBytes[5]
|
LowByte = requestBytes[5]
|
||||||
};
|
};
|
||||||
|
|
||||||
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[0], register, cancellationToken);
|
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[0], register, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
if (isSuccess)
|
if (isSuccess)
|
||||||
{
|
{
|
||||||
// Response is an echo of the request
|
// Response is an echo of the request
|
||||||
@@ -532,8 +548,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
AddCrc(responseBytes);
|
return ReturnResponse(responseBytes);
|
||||||
return [.. responseBytes];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
private async Task<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
@@ -553,8 +568,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes[1] |= 0x80;
|
responseBytes[1] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
|
||||||
AddCrc(responseBytes);
|
return ReturnResponse(responseBytes);
|
||||||
return [.. responseBytes];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -576,7 +590,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[0], coils, cancellationToken);
|
bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[0], coils, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
if (isSuccess)
|
if (isSuccess)
|
||||||
{
|
{
|
||||||
// Response is an echo of the request
|
// Response is an echo of the request
|
||||||
@@ -594,8 +608,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
AddCrc(responseBytes);
|
return ReturnResponse(responseBytes);
|
||||||
return [.. responseBytes];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
private async Task<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
@@ -604,7 +617,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
var responseBytes = new List<byte>();
|
var responseBytes = new List<byte>();
|
||||||
responseBytes.AddRange(requestBytes.Take(8));
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
|
|
||||||
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
ushort count = requestBytes.GetBigEndianUInt16(4);
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
@@ -615,8 +628,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes[1] |= 0x80;
|
responseBytes[1] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
|
||||||
AddCrc(responseBytes);
|
return ReturnResponse(responseBytes);
|
||||||
return [.. responseBytes];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -633,8 +645,9 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
HighByte = requestBytes[baseOffset + i * 2],
|
HighByte = requestBytes[baseOffset + i * 2],
|
||||||
LowByte = requestBytes[baseOffset + i * 2 + 1]
|
LowByte = requestBytes[baseOffset + i * 2 + 1]
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[0], list, cancellationToken);
|
bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[0], list, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
if (isSuccess)
|
if (isSuccess)
|
||||||
{
|
{
|
||||||
// Response is an echo of the request
|
// Response is an echo of the request
|
||||||
@@ -646,19 +659,20 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
responseBytes[1] |= 0x80;
|
responseBytes[1] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
AddCrc(responseBytes);
|
return ReturnResponse(responseBytes);
|
||||||
return [.. responseBytes];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<byte[]> HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
private async Task<byte[]> HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
if (requestBytes.Length < 7)
|
||||||
|
return null;
|
||||||
|
|
||||||
var responseBytes = new List<byte>();
|
var responseBytes = new List<byte>();
|
||||||
responseBytes.AddRange(requestBytes.Take(2));
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
|
|
||||||
@@ -667,8 +681,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes[1] |= 0x80;
|
responseBytes[1] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
|
||||||
|
|
||||||
AddCrc(responseBytes);
|
return ReturnResponse(responseBytes);
|
||||||
return [.. responseBytes];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var firstObject = (ModbusDeviceIdentificationObject)requestBytes[4];
|
var firstObject = (ModbusDeviceIdentificationObject)requestBytes[4];
|
||||||
@@ -677,8 +690,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes[1] |= 0x80;
|
responseBytes[1] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
|
||||||
|
|
||||||
AddCrc(responseBytes);
|
return ReturnResponse(responseBytes);
|
||||||
return [.. responseBytes];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var category = (ModbusDeviceIdentificationCategory)requestBytes[3];
|
var category = (ModbusDeviceIdentificationCategory)requestBytes[3];
|
||||||
@@ -687,13 +699,12 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes[1] |= 0x80;
|
responseBytes[1] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
|
||||||
AddCrc(responseBytes);
|
return ReturnResponse(responseBytes);
|
||||||
return [.. responseBytes];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var res = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken);
|
var deviceInfo = await Client.ReadDeviceIdentificationAsync(requestBytes[0], category, firstObject, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
|
||||||
var bodyBytes = new List<byte>();
|
var bodyBytes = new List<byte>();
|
||||||
|
|
||||||
@@ -702,31 +713,20 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
// Conformity
|
// Conformity
|
||||||
bodyBytes.Add((byte)category);
|
bodyBytes.Add((byte)category);
|
||||||
if (res.IsIndividualAccessAllowed)
|
if (deviceInfo.IsIndividualAccessAllowed)
|
||||||
bodyBytes[2] |= 0x80;
|
bodyBytes[2] |= 0x80;
|
||||||
|
|
||||||
// More, NextId, NumberOfObjects
|
// More, NextId, NumberOfObjects
|
||||||
bodyBytes.AddRange(new byte[3]);
|
bodyBytes.AddRange(new byte[3]);
|
||||||
|
|
||||||
int maxObjectId;
|
int maxObjectId = category switch
|
||||||
switch (category)
|
|
||||||
{
|
{
|
||||||
case ModbusDeviceIdentificationCategory.Basic:
|
ModbusDeviceIdentificationCategory.Basic => 0x02,
|
||||||
maxObjectId = 0x02;
|
ModbusDeviceIdentificationCategory.Regular => 0x06,
|
||||||
break;
|
ModbusDeviceIdentificationCategory.Extended => 0xFF,
|
||||||
|
// Individual
|
||||||
case ModbusDeviceIdentificationCategory.Regular:
|
_ => requestBytes[4],
|
||||||
maxObjectId = 0x06;
|
};
|
||||||
break;
|
|
||||||
|
|
||||||
case ModbusDeviceIdentificationCategory.Extended:
|
|
||||||
maxObjectId = 0xFF;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default: // Individual
|
|
||||||
maxObjectId = requestBytes[4];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte numberOfObjects = 0;
|
byte numberOfObjects = 0;
|
||||||
for (int i = requestBytes[4]; i <= maxObjectId; i++)
|
for (int i = requestBytes[4]; i <= maxObjectId; i++)
|
||||||
@@ -735,17 +735,19 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
if (0x07 <= i && i <= 0x7F)
|
if (0x07 <= i && i <= 0x7F)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
byte[] objBytes = GetDeviceObject((byte)i, res);
|
byte[] objBytes = GetDeviceObject((byte)i, deviceInfo);
|
||||||
|
|
||||||
// We need to split the response if it would exceed the max ADU size
|
// We need to split the response if it would exceed the max ADU size.
|
||||||
if (responseBytes.Count + bodyBytes.Count + objBytes.Length > RtuProtocol.MAX_ADU_LENGTH)
|
// 2 bytes of CRC have to be added.
|
||||||
|
if (responseBytes.Count + bodyBytes.Count + objBytes.Length + 2 > RtuProtocol.MAX_ADU_LENGTH)
|
||||||
{
|
{
|
||||||
bodyBytes[3] = 0xFF;
|
bodyBytes[3] = 0xFF;
|
||||||
bodyBytes[4] = (byte)i;
|
bodyBytes[4] = (byte)i;
|
||||||
|
|
||||||
bodyBytes[5] = numberOfObjects;
|
bodyBytes[5] = numberOfObjects;
|
||||||
responseBytes.AddRange(bodyBytes);
|
responseBytes.AddRange(bodyBytes);
|
||||||
return [.. responseBytes];
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyBytes.AddRange(objBytes);
|
bodyBytes.AddRange(objBytes);
|
||||||
@@ -755,16 +757,14 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
bodyBytes[5] = numberOfObjects;
|
bodyBytes[5] = numberOfObjects;
|
||||||
responseBytes.AddRange(bodyBytes);
|
responseBytes.AddRange(bodyBytes);
|
||||||
|
|
||||||
AddCrc(responseBytes);
|
return ReturnResponse(responseBytes);
|
||||||
return [.. responseBytes];
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
responseBytes[1] |= 0x80;
|
responseBytes[1] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
|
||||||
AddCrc(responseBytes);
|
return ReturnResponse(responseBytes);
|
||||||
return [.. responseBytes];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -775,7 +775,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
{
|
{
|
||||||
case ModbusDeviceIdentificationObject.VendorName:
|
case ModbusDeviceIdentificationObject.VendorName:
|
||||||
{
|
{
|
||||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName);
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName ?? "");
|
||||||
result.Add((byte)bytes.Length);
|
result.Add((byte)bytes.Length);
|
||||||
result.AddRange(bytes);
|
result.AddRange(bytes);
|
||||||
}
|
}
|
||||||
@@ -783,7 +783,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
case ModbusDeviceIdentificationObject.ProductCode:
|
case ModbusDeviceIdentificationObject.ProductCode:
|
||||||
{
|
{
|
||||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode);
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode ?? "");
|
||||||
result.Add((byte)bytes.Length);
|
result.Add((byte)bytes.Length);
|
||||||
result.AddRange(bytes);
|
result.AddRange(bytes);
|
||||||
}
|
}
|
||||||
@@ -791,7 +791,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
case ModbusDeviceIdentificationObject.MajorMinorRevision:
|
case ModbusDeviceIdentificationObject.MajorMinorRevision:
|
||||||
{
|
{
|
||||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision);
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision ?? "");
|
||||||
result.Add((byte)bytes.Length);
|
result.Add((byte)bytes.Length);
|
||||||
result.AddRange(bytes);
|
result.AddRange(bytes);
|
||||||
}
|
}
|
||||||
@@ -799,7 +799,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
case ModbusDeviceIdentificationObject.VendorUrl:
|
case ModbusDeviceIdentificationObject.VendorUrl:
|
||||||
{
|
{
|
||||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl);
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl ?? "");
|
||||||
result.Add((byte)bytes.Length);
|
result.Add((byte)bytes.Length);
|
||||||
result.AddRange(bytes);
|
result.AddRange(bytes);
|
||||||
}
|
}
|
||||||
@@ -807,7 +807,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
case ModbusDeviceIdentificationObject.ProductName:
|
case ModbusDeviceIdentificationObject.ProductName:
|
||||||
{
|
{
|
||||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName);
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName ?? "");
|
||||||
result.Add((byte)bytes.Length);
|
result.Add((byte)bytes.Length);
|
||||||
result.AddRange(bytes);
|
result.AddRange(bytes);
|
||||||
}
|
}
|
||||||
@@ -815,7 +815,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
case ModbusDeviceIdentificationObject.ModelName:
|
case ModbusDeviceIdentificationObject.ModelName:
|
||||||
{
|
{
|
||||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName);
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName ?? "");
|
||||||
result.Add((byte)bytes.Length);
|
result.Add((byte)bytes.Length);
|
||||||
result.AddRange(bytes);
|
result.AddRange(bytes);
|
||||||
}
|
}
|
||||||
@@ -823,7 +823,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
case ModbusDeviceIdentificationObject.UserApplicationName:
|
case ModbusDeviceIdentificationObject.UserApplicationName:
|
||||||
{
|
{
|
||||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName);
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName ?? "");
|
||||||
result.Add((byte)bytes.Length);
|
result.Add((byte)bytes.Length);
|
||||||
result.AddRange(bytes);
|
result.AddRange(bytes);
|
||||||
}
|
}
|
||||||
@@ -831,9 +831,8 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
if (deviceIdentification.ExtendedObjects.ContainsKey(objectId))
|
if (deviceIdentification.ExtendedObjects.TryGetValue(objectId, out byte[] bytes))
|
||||||
{
|
{
|
||||||
byte[] bytes = deviceIdentification.ExtendedObjects[objectId];
|
|
||||||
result.Add((byte)bytes.Length);
|
result.Add((byte)bytes.Length);
|
||||||
result.AddRange(bytes);
|
result.AddRange(bytes);
|
||||||
}
|
}
|
||||||
@@ -848,20 +847,28 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
return [.. result];
|
return [.. result];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SetCrc(byte[] bytes)
|
private static byte[] ReturnResponse(List<byte> response)
|
||||||
{
|
{
|
||||||
byte[] crc = RtuProtocol.CRC16(bytes, 0, bytes.Length - 2);
|
response.AddRange(RtuProtocol.CRC16(response));
|
||||||
bytes[bytes.Length - 2] = crc[0];
|
return [.. response];
|
||||||
bytes[bytes.Length - 1] = crc[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddCrc(List<byte> bytes)
|
|
||||||
{
|
|
||||||
byte[] crc = RtuProtocol.CRC16(bytes);
|
|
||||||
bytes.Add(crc[0]);
|
|
||||||
bytes.Add(crc[1]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Request Handling
|
#endregion Request Handling
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
sb.AppendLine($"RTU Proxy");
|
||||||
|
sb.AppendLine($" {nameof(PortName)}: {PortName}");
|
||||||
|
sb.AppendLine($" {nameof(BaudRate)}: {(int)BaudRate}");
|
||||||
|
sb.AppendLine($" {nameof(DataBits)}: {DataBits}");
|
||||||
|
sb.AppendLine($" {nameof(StopBits)}: {StopBits}");
|
||||||
|
sb.AppendLine($" {nameof(Parity)}: {Parity}");
|
||||||
|
sb.AppendLine($" {nameof(Client)}: {Client.GetType().Name}");
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
94
src/AMWD.Protocols.Modbus.Serial/ModbusRtuServer.cs
Normal file
94
src/AMWD.Protocols.Modbus.Serial/ModbusRtuServer.cs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
using System;
|
||||||
|
using AMWD.Protocols.Modbus.Common;
|
||||||
|
using AMWD.Protocols.Modbus.Common.Events;
|
||||||
|
using AMWD.Protocols.Modbus.Common.Utils;
|
||||||
|
|
||||||
|
namespace AMWD.Protocols.Modbus.Serial
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implements a Modbus serial line RTU server proxying all requests to a virtual Modbus client.
|
||||||
|
/// </summary>
|
||||||
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
|
public class ModbusRtuServer : ModbusRtuProxy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ModbusRtuServer"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="portName">The name of the serial port to use.</param>
|
||||||
|
public ModbusRtuServer(string portName)
|
||||||
|
: base(new VirtualModbusClient(), portName)
|
||||||
|
{
|
||||||
|
TypedClient.CoilWritten += (sender, e) => CoilWritten?.Invoke(this, e);
|
||||||
|
TypedClient.RegisterWritten += (sender, e) => RegisterWritten?.Invoke(this, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Events
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that a <see cref="Coil"/>-value received through a remote client has been written.
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<CoilWrittenEventArgs> CoilWritten;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that a <see cref="HoldingRegister"/>-value received from a remote client has been written.
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<RegisterWrittenEventArgs> RegisterWritten;
|
||||||
|
|
||||||
|
#endregion Events
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
internal VirtualModbusClient TypedClient
|
||||||
|
=> Client as VirtualModbusClient;
|
||||||
|
|
||||||
|
#endregion Properties
|
||||||
|
|
||||||
|
#region Device Handling
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.AddDevice(byte)"/>
|
||||||
|
public bool AddDevice(byte unitId)
|
||||||
|
=> TypedClient.AddDevice(unitId);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.RemoveDevice(byte)"/>
|
||||||
|
public bool RemoveDevice(byte unitId)
|
||||||
|
=> TypedClient.RemoveDevice(unitId);
|
||||||
|
|
||||||
|
#endregion Device Handling
|
||||||
|
|
||||||
|
#region Entity Handling
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.GetCoil(byte, ushort)"/>
|
||||||
|
public Coil GetCoil(byte unitId, ushort address)
|
||||||
|
=> TypedClient.GetCoil(unitId, address);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.SetCoil(byte, Coil)"/>
|
||||||
|
public void SetCoil(byte unitId, Coil coil)
|
||||||
|
=> TypedClient.SetCoil(unitId, coil);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.GetDiscreteInput(byte, ushort)"/>
|
||||||
|
public DiscreteInput GetDiscreteInput(byte unitId, ushort address)
|
||||||
|
=> TypedClient.GetDiscreteInput(unitId, address);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.SetDiscreteInput(byte, DiscreteInput)"/>
|
||||||
|
public void SetDiscreteInput(byte unitId, DiscreteInput discreteInput)
|
||||||
|
=> TypedClient.SetDiscreteInput(unitId, discreteInput);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.GetHoldingRegister(byte, ushort)"/>
|
||||||
|
public HoldingRegister GetHoldingRegister(byte unitId, ushort address)
|
||||||
|
=> TypedClient.GetHoldingRegister(unitId, address);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.SetHoldingRegister(byte, HoldingRegister)"/>
|
||||||
|
public void SetHoldingRegister(byte unitId, HoldingRegister holdingRegister)
|
||||||
|
=> TypedClient.SetHoldingRegister(unitId, holdingRegister);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.GetInputRegister(byte, ushort)"/>
|
||||||
|
public InputRegister GetInputRegister(byte unitId, ushort address)
|
||||||
|
=> TypedClient.GetInputRegister(unitId, address);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.SetInputRegister(byte, InputRegister)"/>
|
||||||
|
public void SetInputRegister(byte unitId, InputRegister inputRegister)
|
||||||
|
=> TypedClient.SetInputRegister(unitId, inputRegister);
|
||||||
|
|
||||||
|
#endregion Entity Handling
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO.Ports;
|
using System.IO.Ports;
|
||||||
|
using System.Text;
|
||||||
using AMWD.Protocols.Modbus.Common.Contracts;
|
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||||
using AMWD.Protocols.Modbus.Common.Protocols;
|
using AMWD.Protocols.Modbus.Common.Protocols;
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="portName">The name of the serial port to use.</param>
|
/// <param name="portName">The name of the serial port to use.</param>
|
||||||
public ModbusSerialClient(string portName)
|
public ModbusSerialClient(string portName)
|
||||||
: this(new ModbusSerialConnection { PortName = portName })
|
: this(new ModbusSerialConnection(portName))
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -40,8 +41,8 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
Protocol = new RtuProtocol();
|
Protocol = new RtuProtocol();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="SerialPort.GetPortNames" />
|
/// <inheritdoc cref="ModbusSerialConnection.AvailablePortNames" />
|
||||||
public static string[] AvailablePortNames => SerialPort.GetPortNames();
|
public static string[] AvailablePortNames => ModbusSerialConnection.AvailablePortNames;
|
||||||
|
|
||||||
/// <inheritdoc cref="IModbusConnection.IdleTimeout"/>
|
/// <inheritdoc cref="IModbusConnection.IdleTimeout"/>
|
||||||
public TimeSpan IdleTimeout
|
public TimeSpan IdleTimeout
|
||||||
@@ -223,5 +224,22 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
serialConnection.StopBits = value;
|
serialConnection.StopBits = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
sb.AppendLine($"Serial Client {PortName}");
|
||||||
|
sb.AppendLine($" {nameof(BaudRate)}: {(int)BaudRate}");
|
||||||
|
sb.AppendLine($" {nameof(DataBits)}: {DataBits}");
|
||||||
|
sb.AppendLine($" {nameof(StopBits)}: {(StopBits == StopBits.OnePointFive ? "1.5" : ((int)StopBits).ToString())}");
|
||||||
|
sb.AppendLine($" {nameof(Parity)}: {Parity.ToString().ToLower()}");
|
||||||
|
sb.AppendLine($" {nameof(Handshake)}: {Handshake.ToString().ToLower()}");
|
||||||
|
sb.AppendLine($" {nameof(RtsEnable)}: {RtsEnable.ToString().ToLower()}");
|
||||||
|
sb.AppendLine($" {nameof(DriverEnabledRS485)}: {DriverEnabledRS485.ToString().ToLower()}");
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,18 +31,24 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
private readonly Task _processingTask;
|
private readonly Task _processingTask;
|
||||||
private readonly AsyncQueue<RequestQueueItem> _requestQueue = new();
|
private readonly AsyncQueue<RequestQueueItem> _requestQueue = new();
|
||||||
|
|
||||||
// Only required to cover all logic branches on unit tests.
|
private readonly bool _isLinux;
|
||||||
private bool _isUnitTest = false;
|
|
||||||
|
|
||||||
#endregion Fields
|
#endregion Fields
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ModbusSerialConnection"/> class.
|
/// Initializes a new instance of the <see cref="ModbusSerialConnection"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ModbusSerialConnection()
|
public ModbusSerialConnection(string portName)
|
||||||
{
|
{
|
||||||
|
_isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(portName))
|
||||||
|
throw new ArgumentNullException(nameof(portName));
|
||||||
|
|
||||||
_serialPort = new SerialPortWrapper
|
_serialPort = new SerialPortWrapper
|
||||||
{
|
{
|
||||||
|
PortName = portName,
|
||||||
|
|
||||||
BaudRate = (int)BaudRate.Baud19200,
|
BaudRate = (int)BaudRate.Baud19200,
|
||||||
DataBits = 8,
|
DataBits = 8,
|
||||||
Handshake = Handshake.None,
|
Handshake = Handshake.None,
|
||||||
@@ -59,6 +65,9 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
|
|
||||||
#region Properties
|
#region Properties
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.GetPortNames" />
|
||||||
|
public static string[] AvailablePortNames => SerialPort.GetPortNames();
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string Name => "Serial";
|
public string Name => "Serial";
|
||||||
|
|
||||||
@@ -68,20 +77,6 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual TimeSpan ConnectTimeout { get; set; } = TimeSpan.MaxValue;
|
public virtual TimeSpan ConnectTimeout { get; set; } = TimeSpan.MaxValue;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public virtual TimeSpan ReadTimeout
|
|
||||||
{
|
|
||||||
get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout);
|
|
||||||
set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public virtual TimeSpan WriteTimeout
|
|
||||||
{
|
|
||||||
get => TimeSpan.FromMilliseconds(_serialPort.WriteTimeout);
|
|
||||||
set => _serialPort.WriteTimeout = (int)value.TotalMilliseconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether the RS485 driver has to be enabled via software switch.
|
/// Gets or sets a value indicating whether the RS485 driver has to be enabled via software switch.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -107,9 +102,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
set => _serialPort.PortName = value;
|
set => _serialPort.PortName = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc cref="SerialPort.BaudRate" />
|
||||||
/// Gets or sets the serial baud rate.
|
|
||||||
/// </summary>
|
|
||||||
public virtual BaudRate BaudRate
|
public virtual BaudRate BaudRate
|
||||||
{
|
{
|
||||||
get => (BaudRate)_serialPort.BaudRate;
|
get => (BaudRate)_serialPort.BaudRate;
|
||||||
@@ -118,7 +111,11 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
|
|
||||||
/// <inheritdoc cref="SerialPort.DataBits" />
|
/// <inheritdoc cref="SerialPort.DataBits" />
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Should be 7 for ASCII mode and 8 for RTU mode.
|
/// From the Specs:
|
||||||
|
/// <br/>
|
||||||
|
/// On <see cref="AsciiProtocol"/> it can be 7 or 8.
|
||||||
|
/// <br/>
|
||||||
|
/// On <see cref="RtuProtocol"/> it has to be 8.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public virtual int DataBits
|
public virtual int DataBits
|
||||||
{
|
{
|
||||||
@@ -159,9 +156,9 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// From the Specs:
|
/// From the Specs:
|
||||||
/// <br/>
|
/// <br/>
|
||||||
/// Should be <see cref="StopBits.One"/> for <see cref="Parity.Even"/> or <see cref="Parity.Odd"/> and
|
/// Should be <see cref="StopBits.One"/> for <see cref="Parity.Even"/> or <see cref="Parity.Odd"/>.
|
||||||
/// <br/>
|
/// <br/>
|
||||||
/// should be <see cref="StopBits.Two"/> for <see cref="Parity.None"/>.
|
/// Should be <see cref="StopBits.Two"/> for <see cref="Parity.None"/>.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public virtual StopBits StopBits
|
public virtual StopBits StopBits
|
||||||
{
|
{
|
||||||
@@ -169,6 +166,20 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
set => _serialPort.StopBits = value;
|
set => _serialPort.StopBits = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public virtual TimeSpan ReadTimeout
|
||||||
|
{
|
||||||
|
get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout);
|
||||||
|
set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public virtual TimeSpan WriteTimeout
|
||||||
|
{
|
||||||
|
get => TimeSpan.FromMilliseconds(_serialPort.WriteTimeout);
|
||||||
|
set => _serialPort.WriteTimeout = (int)value.TotalMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
#endregion SerialPort Properties
|
#endregion SerialPort Properties
|
||||||
|
|
||||||
#endregion Properties
|
#endregion Properties
|
||||||
@@ -188,7 +199,6 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_processingTask.Wait();
|
|
||||||
_processingTask.Dispose();
|
_processingTask.Dispose();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -259,7 +269,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Get next request to process
|
// Get next request to process
|
||||||
var item = await _requestQueue.DequeueAsync(cancellationToken);
|
var item = await _requestQueue.DequeueAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
|
||||||
// Remove registration => already removed from queue
|
// Remove registration => already removed from queue
|
||||||
item.CancellationTokenRegistration.Dispose();
|
item.CancellationTokenRegistration.Dispose();
|
||||||
@@ -267,13 +277,13 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
// Build combined cancellation token
|
// Build combined cancellation token
|
||||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
|
||||||
// Wait for exclusive access
|
// Wait for exclusive access
|
||||||
await _portLock.WaitAsync(linkedCts.Token);
|
await _portLock.WaitAsync(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Ensure connection is up
|
// Ensure connection is up
|
||||||
await AssertConnection(linkedCts.Token);
|
await AssertConnection(linkedCts.Token);
|
||||||
|
|
||||||
await _serialPort.WriteAsync(item.Request, linkedCts.Token);
|
await _serialPort.WriteAsync(item.Request, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
|
||||||
linkedCts.Token.ThrowIfCancellationRequested();
|
linkedCts.Token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
@@ -282,7 +292,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
int readCount = await _serialPort.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token);
|
int readCount = await _serialPort.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
if (readCount < 1)
|
if (readCount < 1)
|
||||||
throw new EndOfStreamException();
|
throw new EndOfStreamException();
|
||||||
|
|
||||||
@@ -313,7 +323,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
_portLock.Release();
|
_portLock.Release();
|
||||||
_idleTimer.Change(IdleTimeout, Timeout.InfiniteTimeSpan);
|
_idleTimer.Change(IdleTimeout, Timeout.InfiniteTimeSpan);
|
||||||
|
|
||||||
await Task.Delay(InterRequestDelay, cancellationToken);
|
await Task.Delay(InterRequestDelay, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
@@ -344,7 +354,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
_serialPort.Close();
|
_serialPort.Close();
|
||||||
_serialPort.ResetRS485DriverStateFlags();
|
_serialPort.ResetRS485DriverStateFlags();
|
||||||
|
|
||||||
if (DriverEnabledRS485 && (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || _isUnitTest))
|
if (DriverEnabledRS485 && _isLinux)
|
||||||
{
|
{
|
||||||
var flags = _serialPort.GetRS485DriverStateFlags();
|
var flags = _serialPort.GetRS485DriverStateFlags();
|
||||||
flags |= RS485Flags.Enabled;
|
flags |= RS485Flags.Enabled;
|
||||||
@@ -352,7 +362,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
_serialPort.ChangeRS485DriverStateFlags(flags);
|
_serialPort.ChangeRS485DriverStateFlags(flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
using var connectTask = Task.Run(_serialPort.Open);
|
using var connectTask = Task.Run(_serialPort.Open, cancellationToken);
|
||||||
if (await Task.WhenAny(connectTask, Task.Delay(ReadTimeout, cancellationToken)) == connectTask)
|
if (await Task.WhenAny(connectTask, Task.Delay(ReadTimeout, cancellationToken)) == connectTask)
|
||||||
{
|
{
|
||||||
await connectTask;
|
await connectTask;
|
||||||
@@ -370,7 +380,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken);
|
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{ /* keep it quiet */ }
|
{ /* keep it quiet */ }
|
||||||
@@ -44,10 +44,10 @@ using var client = new ModbusSerialClient(serialPort)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Published under MIT License (see [**tl;dr**Legal])
|
Published under MIT License (see [choose a license])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[v1.1b3]: https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf
|
[v1.1b3]: https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf
|
||||||
[v1.02]: https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf
|
[v1.02]: https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf
|
||||||
[**tl;dr**Legal]: https://www.tldrlegal.com/license/mit-license
|
[choose a license]: https://choosealicense.com/licenses/mit/
|
||||||
@@ -20,6 +20,30 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
|
|||||||
|
|
||||||
#endregion Fields
|
#endregion Fields
|
||||||
|
|
||||||
|
#region Constructor
|
||||||
|
|
||||||
|
public SerialPortWrapper()
|
||||||
|
{
|
||||||
|
_serialPort.DataReceived += (sender, e) => DataReceived?.Invoke(this, e);
|
||||||
|
_serialPort.PinChanged += (sender, e) => PinChanged?.Invoke(this, e);
|
||||||
|
_serialPort.ErrorReceived += (sender, e) => ErrorReceived?.Invoke(this, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Constructor
|
||||||
|
|
||||||
|
#region Events
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.DataReceived"/>
|
||||||
|
public virtual event SerialDataReceivedEventHandler DataReceived;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.PinChanged"/>
|
||||||
|
public virtual event SerialPinChangedEventHandler PinChanged;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.ErrorReceived"/>
|
||||||
|
public virtual event SerialErrorReceivedEventHandler ErrorReceived;
|
||||||
|
|
||||||
|
#endregion Events
|
||||||
|
|
||||||
#region Properties
|
#region Properties
|
||||||
|
|
||||||
/// <inheritdoc cref="SerialPort.Handshake"/>
|
/// <inheritdoc cref="SerialPort.Handshake"/>
|
||||||
@@ -82,6 +106,10 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
|
|||||||
set => _serialPort.Parity = value;
|
set => _serialPort.Parity = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.BytesToWrite"/>
|
||||||
|
public virtual int BytesToWrite
|
||||||
|
=> _serialPort.BytesToWrite;
|
||||||
|
|
||||||
/// <inheritdoc cref="SerialPort.BaudRate"/>
|
/// <inheritdoc cref="SerialPort.BaudRate"/>
|
||||||
public virtual int BaudRate
|
public virtual int BaudRate
|
||||||
{
|
{
|
||||||
@@ -89,6 +117,10 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
|
|||||||
set => _serialPort.BaudRate = value;
|
set => _serialPort.BaudRate = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.BytesToRead"/>
|
||||||
|
public virtual int BytesToRead
|
||||||
|
=> _serialPort.BytesToRead;
|
||||||
|
|
||||||
#endregion Properties
|
#endregion Properties
|
||||||
|
|
||||||
#region Methods
|
#region Methods
|
||||||
@@ -101,6 +133,14 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
|
|||||||
public virtual void Open()
|
public virtual void Open()
|
||||||
=> _serialPort.Open();
|
=> _serialPort.Open();
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.Read(byte[], int, int)"/>
|
||||||
|
public virtual int Read(byte[] buffer, int offset, int count)
|
||||||
|
=> _serialPort.Read(buffer, offset, count);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.Write(byte[], int, int)"/>
|
||||||
|
public virtual void Write(byte[] buffer, int offset, int count)
|
||||||
|
=> _serialPort.Write(buffer, offset, count);
|
||||||
|
|
||||||
/// <inheritdoc cref="SerialPort.Dispose"/>
|
/// <inheritdoc cref="SerialPort.Dispose"/>
|
||||||
public virtual void Dispose()
|
public virtual void Dispose()
|
||||||
=> _serialPort.Dispose();
|
=> _serialPort.Dispose();
|
||||||
@@ -117,7 +157,7 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// There seems to be a bug with the async stream implementation on Windows.
|
/// There seems to be a bug with the async stream implementation on Windows.
|
||||||
/// <br/>
|
/// <br/>
|
||||||
/// See this StackOverflow answer: <see href="https://stackoverflow.com/a/54610437/11906695" />
|
/// See this StackOverflow answer: <see href="https://stackoverflow.com/a/54610437/11906695" />.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="buffer">The buffer to write the data into.</param>
|
/// <param name="buffer">The buffer to write the data into.</param>
|
||||||
/// <param name="offset">The byte offset in buffer at which to begin writing data from the serial port.</param>
|
/// <param name="offset">The byte offset in buffer at which to begin writing data from the serial port.</param>
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
|
||||||
<LangVersion>12.0</LangVersion>
|
|
||||||
|
|
||||||
<PackageId>AMWD.Protocols.Modbus.Tcp</PackageId>
|
<PackageId>AMWD.Protocols.Modbus.Tcp</PackageId>
|
||||||
<AssemblyName>amwd-modbus-tcp</AssemblyName>
|
<AssemblyName>amwd-modbus-tcp</AssemblyName>
|
||||||
<RootNamespace>AMWD.Protocols.Modbus.Tcp</RootNamespace>
|
<RootNamespace>AMWD.Protocols.Modbus.Tcp</RootNamespace>
|
||||||
@@ -14,17 +11,12 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="../AMWD.Protocols.Modbus.Common/InternalsVisibleTo.cs" Link="InternalsVisibleTo.cs" />
|
|
||||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" />
|
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" />
|
||||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ReaderWriterLockSlimExtensions.cs" Link="Extensions/ReaderWriterLockSlimExtensions.cs" />
|
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ReaderWriterLockSlimExtensions.cs" Link="Extensions/ReaderWriterLockSlimExtensions.cs" />
|
||||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/AsyncQueue.cs" Link="Utils/AsyncQueue.cs" />
|
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/AsyncQueue.cs" Link="Utils/AsyncQueue.cs" />
|
||||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/RequestQueueItem.cs" Link="Utils/RequestQueueItem.cs" />
|
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/RequestQueueItem.cs" Link="Utils/RequestQueueItem.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="README.md" Pack="true" PackagePath="/" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
45
src/AMWD.Protocols.Modbus.Tcp/Extensions/StreamExtensions.cs
Normal file
45
src/AMWD.Protocols.Modbus.Tcp/Extensions/StreamExtensions.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AMWD.Protocols.Modbus.Tcp.Utils;
|
||||||
|
|
||||||
|
namespace System.IO
|
||||||
|
{
|
||||||
|
internal static class StreamExtensions
|
||||||
|
{
|
||||||
|
public static async Task<byte[]> ReadExpectedBytesAsync(this Stream stream, int expectedBytes, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[expectedBytes];
|
||||||
|
int offset = 0;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
if (count < 1)
|
||||||
|
throw new EndOfStreamException();
|
||||||
|
|
||||||
|
offset += count;
|
||||||
|
}
|
||||||
|
while (offset < expectedBytes && !cancellationToken.IsCancellationRequested);
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<byte[]> ReadExpectedBytesAsync(this NetworkStreamWrapper stream, int expectedBytes, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[expectedBytes];
|
||||||
|
int offset = 0;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
if (count < 1)
|
||||||
|
throw new EndOfStreamException();
|
||||||
|
|
||||||
|
offset += count;
|
||||||
|
}
|
||||||
|
while (offset < expectedBytes && !cancellationToken.IsCancellationRequested);
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/AMWD.Protocols.Modbus.Tcp/Extensions/TaskExtensions.cs
Normal file
17
src/AMWD.Protocols.Modbus.Tcp/Extensions/TaskExtensions.cs
Normal 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 */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Text;
|
||||||
using AMWD.Protocols.Modbus.Common.Contracts;
|
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||||
using AMWD.Protocols.Modbus.Common.Protocols;
|
using AMWD.Protocols.Modbus.Common.Protocols;
|
||||||
|
|
||||||
@@ -101,5 +102,16 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
tcpConnection.Port = value;
|
tcpConnection.Port = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
sb.AppendLine($"TCP Client {Hostname}");
|
||||||
|
sb.AppendLine($" {nameof(Port)}: {Port}");
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,8 +34,8 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
private readonly Task _processingTask;
|
private readonly Task _processingTask;
|
||||||
private readonly AsyncQueue<RequestQueueItem> _requestQueue = new();
|
private readonly AsyncQueue<RequestQueueItem> _requestQueue = new();
|
||||||
|
|
||||||
private TimeSpan _readTimeout = TimeSpan.FromMilliseconds(1);
|
private TimeSpan _readTimeout = TimeSpan.FromSeconds(1);
|
||||||
private TimeSpan _writeTimeout = TimeSpan.FromMilliseconds(1);
|
private TimeSpan _writeTimeout = TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
#endregion Fields
|
#endregion Fields
|
||||||
|
|
||||||
@@ -65,8 +65,12 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
get => _readTimeout;
|
get => _readTimeout;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
ArgumentOutOfRangeException.ThrowIfLessThan(value, TimeSpan.Zero);
|
||||||
|
#else
|
||||||
if (value < TimeSpan.Zero)
|
if (value < TimeSpan.Zero)
|
||||||
throw new ArgumentOutOfRangeException(nameof(value));
|
throw new ArgumentOutOfRangeException(nameof(value));
|
||||||
|
#endif
|
||||||
|
|
||||||
_readTimeout = value;
|
_readTimeout = value;
|
||||||
|
|
||||||
@@ -81,8 +85,12 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
get => _writeTimeout;
|
get => _writeTimeout;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
ArgumentOutOfRangeException.ThrowIfLessThan(value, TimeSpan.Zero);
|
||||||
|
#else
|
||||||
if (value < TimeSpan.Zero)
|
if (value < TimeSpan.Zero)
|
||||||
throw new ArgumentOutOfRangeException(nameof(value));
|
throw new ArgumentOutOfRangeException(nameof(value));
|
||||||
|
#endif
|
||||||
|
|
||||||
_writeTimeout = value;
|
_writeTimeout = value;
|
||||||
|
|
||||||
@@ -208,7 +216,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Get next request to process
|
// Get next request to process
|
||||||
var item = await _requestQueue.DequeueAsync(cancellationToken);
|
var item = await _requestQueue.DequeueAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
|
||||||
// Remove registration => already removed from queue
|
// Remove registration => already removed from queue
|
||||||
item.CancellationTokenRegistration.Dispose();
|
item.CancellationTokenRegistration.Dispose();
|
||||||
@@ -216,19 +224,19 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
// Build combined cancellation token
|
// Build combined cancellation token
|
||||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
|
||||||
// Wait for exclusive access
|
// Wait for exclusive access
|
||||||
await _clientLock.WaitAsync(linkedCts.Token);
|
await _clientLock.WaitAsync(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Ensure connection is up
|
// Ensure connection is up
|
||||||
await AssertConnection(linkedCts.Token);
|
await AssertConnection(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
|
||||||
var stream = _tcpClient.GetStream();
|
var stream = _tcpClient.GetStream();
|
||||||
await stream.FlushAsync(linkedCts.Token);
|
await stream.FlushAsync(linkedCts.Token);
|
||||||
|
|
||||||
#if NET6_0_OR_GREATER
|
#if NET6_0_OR_GREATER
|
||||||
await stream.WriteAsync(item.Request, linkedCts.Token);
|
await stream.WriteAsync(item.Request, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
#else
|
#else
|
||||||
await stream.WriteAsync(item.Request, 0, item.Request.Length, linkedCts.Token);
|
await stream.WriteAsync(item.Request, 0, item.Request.Length, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
linkedCts.Token.ThrowIfCancellationRequested();
|
linkedCts.Token.ThrowIfCancellationRequested();
|
||||||
@@ -239,9 +247,9 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
do
|
do
|
||||||
{
|
{
|
||||||
#if NET6_0_OR_GREATER
|
#if NET6_0_OR_GREATER
|
||||||
int readCount = await stream.ReadAsync(buffer, linkedCts.Token);
|
int readCount = await stream.ReadAsync(buffer, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
#else
|
#else
|
||||||
int readCount = await stream.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token);
|
int readCount = await stream.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
#endif
|
#endif
|
||||||
if (readCount < 1)
|
if (readCount < 1)
|
||||||
throw new EndOfStreamException();
|
throw new EndOfStreamException();
|
||||||
@@ -332,7 +340,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken);
|
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{ /* keep it quiet */ }
|
{ /* keep it quiet */ }
|
||||||
@@ -376,10 +384,9 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return Dns.GetHostAddresses(hostname)
|
return [.. Dns.GetHostAddresses(hostname)
|
||||||
.Where(a => a.AddressFamily == AddressFamily.InterNetwork || a.AddressFamily == AddressFamily.InterNetworkV6)
|
.Where(a => a.AddressFamily == AddressFamily.InterNetwork || a.AddressFamily == AddressFamily.InterNetworkV6)
|
||||||
.OrderBy(a => a.AddressFamily) // prefer IPv4
|
.OrderBy(a => a.AddressFamily)]; // prefer IPv4
|
||||||
.ToArray();
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -10,62 +10,38 @@ using System.Threading.Tasks;
|
|||||||
using AMWD.Protocols.Modbus.Common;
|
using AMWD.Protocols.Modbus.Common;
|
||||||
using AMWD.Protocols.Modbus.Common.Contracts;
|
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||||
using AMWD.Protocols.Modbus.Common.Protocols;
|
using AMWD.Protocols.Modbus.Common.Protocols;
|
||||||
|
using AMWD.Protocols.Modbus.Tcp.Extensions;
|
||||||
|
using AMWD.Protocols.Modbus.Tcp.Utils;
|
||||||
|
|
||||||
namespace AMWD.Protocols.Modbus.Proxy
|
namespace AMWD.Protocols.Modbus.Tcp
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implements a Modbus TCP server proxying all requests to a Modbus client of choice.
|
/// Implements a Modbus TCP server proxying all requests to a Modbus client of choice.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ModbusTcpProxy : IDisposable
|
/// <remarks>
|
||||||
|
/// Initializes a new instance of the <see cref="ModbusTcpProxy"/> class.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="client">The <see cref="ModbusClientBase"/> used to request the remote device, that should be proxied.</param>
|
||||||
|
/// <param name="listenAddress">An <see cref="IPAddress"/> to listen on.</param>
|
||||||
|
public class ModbusTcpProxy(ModbusClientBase client, IPAddress listenAddress) : IModbusProxy
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
|
|
||||||
private bool _isDisposed;
|
private bool _isDisposed;
|
||||||
|
|
||||||
private TcpListener _listener;
|
private TimeSpan _readWriteTimeout = TimeSpan.FromSeconds(100);
|
||||||
|
|
||||||
|
private readonly TcpListenerWrapper _tcpListener = new(listenAddress, 502);
|
||||||
private CancellationTokenSource _stopCts;
|
private CancellationTokenSource _stopCts;
|
||||||
private Task _clientConnectTask = Task.CompletedTask;
|
private Task _clientConnectTask = Task.CompletedTask;
|
||||||
|
|
||||||
private readonly SemaphoreSlim _clientListLock = new(1, 1);
|
private readonly SemaphoreSlim _clientListLock = new(1, 1);
|
||||||
private readonly List<TcpClient> _clients = [];
|
private readonly List<TcpClientWrapper> _clients = [];
|
||||||
private readonly List<Task> _clientTasks = [];
|
|
||||||
|
|
||||||
#endregion Fields
|
#endregion Fields
|
||||||
|
|
||||||
#region Constructors
|
#region Constructors
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ModbusTcpProxy"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="client">The <see cref="ModbusClientBase"/> used to request the remote device, that should be proxied.</param>
|
|
||||||
/// <param name="listenAddress">An <see cref="IPAddress"/> to listen on (Default: <see cref="IPAddress.Loopback"/>).</param>
|
|
||||||
/// <param name="listenPort">A port to listen on (Default: 502).</param>
|
|
||||||
public ModbusTcpProxy(ModbusClientBase client, IPAddress listenAddress = null, int listenPort = 502)
|
|
||||||
{
|
|
||||||
Client = client ?? throw new ArgumentNullException(nameof(client));
|
|
||||||
|
|
||||||
ListenAddress = listenAddress ?? IPAddress.Loopback;
|
|
||||||
|
|
||||||
if (ushort.MinValue < listenPort || listenPort < ushort.MaxValue)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(listenPort));
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
#if NET8_0_OR_GREATER
|
|
||||||
using var testListener = new TcpListener(ListenAddress, listenPort);
|
|
||||||
#else
|
|
||||||
var testListener = new TcpListener(ListenAddress, listenPort);
|
|
||||||
#endif
|
|
||||||
testListener.Start(1);
|
|
||||||
ListenPort = (testListener.LocalEndpoint as IPEndPoint).Port;
|
|
||||||
testListener.Stop();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"{nameof(ListenPort)} ({listenPort}) is already in use.", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Constructors
|
#endregion Constructors
|
||||||
|
|
||||||
#region Properties
|
#region Properties
|
||||||
@@ -73,27 +49,46 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the Modbus client used to request the remote device, that should be proxied.
|
/// Gets the Modbus client used to request the remote device, that should be proxied.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ModbusClientBase Client { get; }
|
public ModbusClientBase Client { get; } = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the <see cref="IPAddress"/> to listen on.
|
/// Gets the <see cref="IPAddress"/> to listen on.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IPAddress ListenAddress { get; }
|
public IPAddress ListenAddress
|
||||||
|
{
|
||||||
|
get => _tcpListener.LocalIPEndPoint.Address;
|
||||||
|
set => _tcpListener.LocalIPEndPoint.Address = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the port to listen on.
|
/// Get the port to listen on.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ListenPort { get; }
|
public int ListenPort
|
||||||
|
{
|
||||||
|
get => _tcpListener.LocalIPEndPoint.Port;
|
||||||
|
set => _tcpListener.LocalIPEndPoint.Port = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether the server is running.
|
/// Gets a value indicating whether the server is running.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsRunning => _listener?.Server.IsBound ?? false;
|
public bool IsRunning => _tcpListener.Socket.IsBound;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the read/write timeout for the incoming connections (not the <see cref="Client"/>!).
|
/// Gets or sets the read/write timeout for the incoming connections (not the <see cref="Client"/>!).
|
||||||
|
/// Default: 100 seconds.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan ReadWriteTimeout { get; set; }
|
public TimeSpan ReadWriteTimeout
|
||||||
|
{
|
||||||
|
get => _readWriteTimeout;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value != Timeout.InfiniteTimeSpan && value < TimeSpan.Zero)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(value));
|
||||||
|
|
||||||
|
_readWriteTimeout = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endregion Properties
|
#endregion Properties
|
||||||
|
|
||||||
@@ -108,20 +103,17 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
Assertions();
|
Assertions();
|
||||||
|
|
||||||
_stopCts?.Cancel();
|
_stopCts?.Cancel();
|
||||||
|
_tcpListener.Stop();
|
||||||
_listener?.Stop();
|
|
||||||
#if NET8_0_OR_GREATER
|
|
||||||
_listener?.Dispose();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
_stopCts?.Dispose();
|
_stopCts?.Dispose();
|
||||||
_stopCts = new CancellationTokenSource();
|
_stopCts = new CancellationTokenSource();
|
||||||
|
|
||||||
_listener = new TcpListener(ListenAddress, ListenPort);
|
// Only allowed to set, if the socket is in the InterNetworkV6 address family.
|
||||||
|
// See: https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.dualmode?view=netstandard-2.0#exceptions
|
||||||
if (ListenAddress.AddressFamily == AddressFamily.InterNetworkV6)
|
if (ListenAddress.AddressFamily == AddressFamily.InterNetworkV6)
|
||||||
_listener.Server.DualMode = true;
|
_tcpListener.Socket.DualMode = true;
|
||||||
|
|
||||||
_listener.Start();
|
_tcpListener.Start();
|
||||||
_clientConnectTask = WaitForClientAsync(_stopCts.Token);
|
_clientConnectTask = WaitForClientAsync(_stopCts.Token);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -139,24 +131,12 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
private async Task StopAsyncInternal(CancellationToken cancellationToken = default)
|
private async Task StopAsyncInternal(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
_stopCts.Cancel();
|
_stopCts?.Cancel();
|
||||||
|
_tcpListener.Stop();
|
||||||
_listener.Stop();
|
|
||||||
#if NET8_0_OR_GREATER
|
|
||||||
_listener.Dispose();
|
|
||||||
#endif
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Task.WhenAny(_clientConnectTask, Task.Delay(Timeout.Infinite, cancellationToken));
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
// Terminated
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.WhenAny(Task.WhenAll(_clientTasks), Task.Delay(Timeout.Infinite, cancellationToken));
|
await Task.WhenAny(_clientConnectTask, Task.Delay(Timeout.Infinite, cancellationToken)).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -178,8 +158,10 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
_clientListLock.Dispose();
|
_clientListLock.Dispose();
|
||||||
_clients.Clear();
|
_clients.Clear();
|
||||||
|
_tcpListener.Dispose();
|
||||||
|
|
||||||
_stopCts?.Dispose();
|
_stopCts?.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Assertions()
|
private void Assertions()
|
||||||
@@ -202,16 +184,13 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
#if NET8_0_OR_GREATER
|
var client = await _tcpListener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
var client = await _listener.AcceptTcpClientAsync(cancellationToken);
|
await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
#else
|
|
||||||
var client = await _listener.AcceptTcpClientAsync();
|
|
||||||
#endif
|
|
||||||
await _clientListLock.WaitAsync(cancellationToken);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_clients.Add(client);
|
_clients.Add(client);
|
||||||
_clientTasks.Add(HandleClientAsync(client, cancellationToken));
|
// Can be ignored as it will terminate by itself on cancellation
|
||||||
|
HandleClientAsync(client, cancellationToken).Forget();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -225,7 +204,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken)
|
private async Task HandleClientAsync(TcpClientWrapper client, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -234,23 +213,32 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
{
|
{
|
||||||
var requestBytes = new List<byte>();
|
var requestBytes = new List<byte>();
|
||||||
|
|
||||||
|
// Waiting for next request
|
||||||
|
byte[] headerBytes = await stream.ReadExpectedBytesAsync(6, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
requestBytes.AddRange(headerBytes);
|
||||||
|
|
||||||
|
ushort length = headerBytes
|
||||||
|
.Skip(4).Take(2).ToArray()
|
||||||
|
.GetBigEndianUInt16();
|
||||||
|
|
||||||
|
// Waiting for the remaining required data
|
||||||
using (var cts = new CancellationTokenSource(ReadWriteTimeout))
|
using (var cts = new CancellationTokenSource(ReadWriteTimeout))
|
||||||
using (cancellationToken.Register(cts.Cancel))
|
using (cancellationToken.Register(cts.Cancel))
|
||||||
{
|
{
|
||||||
byte[] headerBytes = await stream.ReadExpectedBytesAsync(6, cts.Token);
|
byte[] bodyBytes = await stream.ReadExpectedBytesAsync(length, cts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
requestBytes.AddRange(headerBytes);
|
|
||||||
|
|
||||||
byte[] followingCountBytes = headerBytes.Skip(4).Take(2).ToArray();
|
|
||||||
followingCountBytes.SwapBigEndian();
|
|
||||||
int followingCount = BitConverter.ToUInt16(followingCountBytes, 0);
|
|
||||||
|
|
||||||
byte[] bodyBytes = await stream.ReadExpectedBytesAsync(followingCount, cts.Token);
|
|
||||||
requestBytes.AddRange(bodyBytes);
|
requestBytes.AddRange(bodyBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] responseBytes = await HandleRequestAsync([.. requestBytes], cancellationToken);
|
byte[] responseBytes = await HandleRequestAsync([.. requestBytes], cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
if (responseBytes != null)
|
if (responseBytes != null)
|
||||||
await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken);
|
{
|
||||||
|
// Write response when available
|
||||||
|
using (var cts = new CancellationTokenSource(ReadWriteTimeout))
|
||||||
|
using (cancellationToken.Register(cts.Cancel))
|
||||||
|
{
|
||||||
|
await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -259,7 +247,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
await _clientListLock.WaitAsync(cancellationToken);
|
await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_clients.Remove(client);
|
_clients.Remove(client);
|
||||||
@@ -309,14 +297,14 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
default: // unknown function
|
default: // unknown function
|
||||||
{
|
{
|
||||||
byte[] responseBytes = new byte[9];
|
var responseBytes = new List<byte>();
|
||||||
Array.Copy(requestBytes, 0, responseBytes, 0, 8);
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
|
||||||
|
|
||||||
// Mark as error
|
// Mark as error
|
||||||
responseBytes[7] |= 0x80;
|
responseBytes[7] |= 0x80;
|
||||||
|
|
||||||
responseBytes[8] = (byte)ModbusErrorCode.IllegalFunction;
|
return Task.FromResult(ReturnResponse(responseBytes));
|
||||||
return Task.FromResult(responseBytes);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,7 +322,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.AddRange(requestBytes.Take(8));
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken);
|
var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
|
||||||
byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)];
|
byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)];
|
||||||
for (int i = 0; i < coils.Count; i++)
|
for (int i = 0; i < coils.Count; i++)
|
||||||
@@ -357,7 +345,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [.. responseBytes];
|
return ReturnResponse(responseBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<byte[]> HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
private async Task<byte[]> HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
@@ -373,7 +361,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.AddRange(requestBytes.Take(8));
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken);
|
var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
|
||||||
byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)];
|
byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)];
|
||||||
for (int i = 0; i < discreteInputs.Count; i++)
|
for (int i = 0; i < discreteInputs.Count; i++)
|
||||||
@@ -396,7 +384,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [.. responseBytes];
|
return ReturnResponse(responseBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<byte[]> HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
private async Task<byte[]> HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
@@ -412,7 +400,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.AddRange(requestBytes.Take(8));
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken);
|
var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
|
||||||
byte[] values = new byte[holdingRegisters.Count * 2];
|
byte[] values = new byte[holdingRegisters.Count * 2];
|
||||||
for (int i = 0; i < holdingRegisters.Count; i++)
|
for (int i = 0; i < holdingRegisters.Count; i++)
|
||||||
@@ -430,7 +418,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [.. responseBytes];
|
return ReturnResponse(responseBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<byte[]> HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
private async Task<byte[]> HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
@@ -446,7 +434,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.AddRange(requestBytes.Take(8));
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken);
|
var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
|
||||||
byte[] values = new byte[count * 2];
|
byte[] values = new byte[count * 2];
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
@@ -464,7 +452,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [.. responseBytes];
|
return ReturnResponse(responseBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
private async Task<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
@@ -481,7 +469,8 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
{
|
{
|
||||||
responseBytes[7] |= 0x80;
|
responseBytes[7] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
return [.. responseBytes];
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -493,7 +482,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
LowByte = requestBytes[11],
|
LowByte = requestBytes[11],
|
||||||
};
|
};
|
||||||
|
|
||||||
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[6], coil, cancellationToken);
|
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[6], coil, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
if (isSuccess)
|
if (isSuccess)
|
||||||
{
|
{
|
||||||
// Response is an echo of the request
|
// Response is an echo of the request
|
||||||
@@ -511,7 +500,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [.. responseBytes];
|
return ReturnResponse(responseBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
private async Task<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
@@ -533,7 +522,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
LowByte = requestBytes[11]
|
LowByte = requestBytes[11]
|
||||||
};
|
};
|
||||||
|
|
||||||
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[6], register, cancellationToken);
|
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[6], register, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
if (isSuccess)
|
if (isSuccess)
|
||||||
{
|
{
|
||||||
// Response is an echo of the request
|
// Response is an echo of the request
|
||||||
@@ -551,7 +540,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [.. responseBytes];
|
return ReturnResponse(responseBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
private async Task<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
@@ -570,7 +559,8 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
{
|
{
|
||||||
responseBytes[7] |= 0x80;
|
responseBytes[7] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
return [.. responseBytes];
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -592,7 +582,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[6], coils, cancellationToken);
|
bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[6], coils, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
if (isSuccess)
|
if (isSuccess)
|
||||||
{
|
{
|
||||||
// Response is an echo of the request
|
// Response is an echo of the request
|
||||||
@@ -610,7 +600,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [.. responseBytes];
|
return ReturnResponse(responseBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
private async Task<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
@@ -629,7 +619,8 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
{
|
{
|
||||||
responseBytes[7] |= 0x80;
|
responseBytes[7] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
return [.. responseBytes];
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -646,8 +637,9 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
HighByte = requestBytes[baseOffset + i * 2],
|
HighByte = requestBytes[baseOffset + i * 2],
|
||||||
LowByte = requestBytes[baseOffset + i * 2 + 1]
|
LowByte = requestBytes[baseOffset + i * 2 + 1]
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[6], list, cancellationToken);
|
bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[6], list, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
if (isSuccess)
|
if (isSuccess)
|
||||||
{
|
{
|
||||||
// Response is an echo of the request
|
// Response is an echo of the request
|
||||||
@@ -659,18 +651,20 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
responseBytes[7] |= 0x80;
|
responseBytes[7] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [.. responseBytes];
|
return ReturnResponse(responseBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<byte[]> HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
private async Task<byte[]> HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
if (requestBytes.Length < 11)
|
||||||
|
return null;
|
||||||
|
|
||||||
var responseBytes = new List<byte>();
|
var responseBytes = new List<byte>();
|
||||||
responseBytes.AddRange(requestBytes.Take(8));
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
|
||||||
@@ -678,7 +672,8 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
{
|
{
|
||||||
responseBytes[7] |= 0x80;
|
responseBytes[7] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
|
||||||
return [.. responseBytes];
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
var firstObject = (ModbusDeviceIdentificationObject)requestBytes[10];
|
var firstObject = (ModbusDeviceIdentificationObject)requestBytes[10];
|
||||||
@@ -686,7 +681,8 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
{
|
{
|
||||||
responseBytes[7] |= 0x80;
|
responseBytes[7] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
|
||||||
return [.. responseBytes];
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
var category = (ModbusDeviceIdentificationCategory)requestBytes[9];
|
var category = (ModbusDeviceIdentificationCategory)requestBytes[9];
|
||||||
@@ -694,12 +690,13 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
{
|
{
|
||||||
responseBytes[7] |= 0x80;
|
responseBytes[7] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
return [.. responseBytes];
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var res = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken);
|
var deviceInfo = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
|
||||||
var bodyBytes = new List<byte>();
|
var bodyBytes = new List<byte>();
|
||||||
|
|
||||||
@@ -708,31 +705,20 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
// Conformity
|
// Conformity
|
||||||
bodyBytes.Add((byte)category);
|
bodyBytes.Add((byte)category);
|
||||||
if (res.IsIndividualAccessAllowed)
|
if (deviceInfo.IsIndividualAccessAllowed)
|
||||||
bodyBytes[2] |= 0x80;
|
bodyBytes[2] |= 0x80;
|
||||||
|
|
||||||
// More, NextId, NumberOfObjects
|
// More, NextId, NumberOfObjects
|
||||||
bodyBytes.AddRange(new byte[3]);
|
bodyBytes.AddRange(new byte[3]);
|
||||||
|
|
||||||
int maxObjectId;
|
int maxObjectId = category switch
|
||||||
switch (category)
|
|
||||||
{
|
{
|
||||||
case ModbusDeviceIdentificationCategory.Basic:
|
ModbusDeviceIdentificationCategory.Basic => 0x02,
|
||||||
maxObjectId = 0x02;
|
ModbusDeviceIdentificationCategory.Regular => 0x06,
|
||||||
break;
|
ModbusDeviceIdentificationCategory.Extended => 0xFF,
|
||||||
|
// Individual
|
||||||
case ModbusDeviceIdentificationCategory.Regular:
|
_ => requestBytes[10],
|
||||||
maxObjectId = 0x06;
|
};
|
||||||
break;
|
|
||||||
|
|
||||||
case ModbusDeviceIdentificationCategory.Extended:
|
|
||||||
maxObjectId = 0xFF;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default: // Individual
|
|
||||||
maxObjectId = requestBytes[10];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte numberOfObjects = 0;
|
byte numberOfObjects = 0;
|
||||||
for (int i = requestBytes[10]; i <= maxObjectId; i++)
|
for (int i = requestBytes[10]; i <= maxObjectId; i++)
|
||||||
@@ -741,7 +727,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
if (0x07 <= i && i <= 0x7F)
|
if (0x07 <= i && i <= 0x7F)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
byte[] objBytes = GetDeviceObject((byte)i, res);
|
byte[] objBytes = GetDeviceObject((byte)i, deviceInfo);
|
||||||
|
|
||||||
// We need to split the response if it would exceed the max ADU size
|
// We need to split the response if it would exceed the max ADU size
|
||||||
if (responseBytes.Count + bodyBytes.Count + objBytes.Length > TcpProtocol.MAX_ADU_LENGTH)
|
if (responseBytes.Count + bodyBytes.Count + objBytes.Length > TcpProtocol.MAX_ADU_LENGTH)
|
||||||
@@ -751,7 +737,8 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
bodyBytes[5] = numberOfObjects;
|
bodyBytes[5] = numberOfObjects;
|
||||||
responseBytes.AddRange(bodyBytes);
|
responseBytes.AddRange(bodyBytes);
|
||||||
return [.. responseBytes];
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyBytes.AddRange(objBytes);
|
bodyBytes.AddRange(objBytes);
|
||||||
@@ -760,24 +747,26 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
bodyBytes[5] = numberOfObjects;
|
bodyBytes[5] = numberOfObjects;
|
||||||
responseBytes.AddRange(bodyBytes);
|
responseBytes.AddRange(bodyBytes);
|
||||||
return [.. responseBytes];
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
responseBytes[7] |= 0x80;
|
responseBytes[7] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
return [.. responseBytes];
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] GetDeviceObject(byte objectId, DeviceIdentification deviceIdentification)
|
private static byte[] GetDeviceObject(byte objectId, DeviceIdentification deviceIdentification)
|
||||||
{
|
{
|
||||||
var result = new List<byte> { objectId };
|
var result = new List<byte> { objectId };
|
||||||
switch ((ModbusDeviceIdentificationObject)objectId)
|
switch ((ModbusDeviceIdentificationObject)objectId)
|
||||||
{
|
{
|
||||||
case ModbusDeviceIdentificationObject.VendorName:
|
case ModbusDeviceIdentificationObject.VendorName:
|
||||||
{
|
{
|
||||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName);
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName ?? "");
|
||||||
result.Add((byte)bytes.Length);
|
result.Add((byte)bytes.Length);
|
||||||
result.AddRange(bytes);
|
result.AddRange(bytes);
|
||||||
}
|
}
|
||||||
@@ -785,7 +774,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
case ModbusDeviceIdentificationObject.ProductCode:
|
case ModbusDeviceIdentificationObject.ProductCode:
|
||||||
{
|
{
|
||||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode);
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode ?? "");
|
||||||
result.Add((byte)bytes.Length);
|
result.Add((byte)bytes.Length);
|
||||||
result.AddRange(bytes);
|
result.AddRange(bytes);
|
||||||
}
|
}
|
||||||
@@ -793,7 +782,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
case ModbusDeviceIdentificationObject.MajorMinorRevision:
|
case ModbusDeviceIdentificationObject.MajorMinorRevision:
|
||||||
{
|
{
|
||||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision);
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision ?? "");
|
||||||
result.Add((byte)bytes.Length);
|
result.Add((byte)bytes.Length);
|
||||||
result.AddRange(bytes);
|
result.AddRange(bytes);
|
||||||
}
|
}
|
||||||
@@ -801,7 +790,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
case ModbusDeviceIdentificationObject.VendorUrl:
|
case ModbusDeviceIdentificationObject.VendorUrl:
|
||||||
{
|
{
|
||||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl);
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl ?? "");
|
||||||
result.Add((byte)bytes.Length);
|
result.Add((byte)bytes.Length);
|
||||||
result.AddRange(bytes);
|
result.AddRange(bytes);
|
||||||
}
|
}
|
||||||
@@ -809,7 +798,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
case ModbusDeviceIdentificationObject.ProductName:
|
case ModbusDeviceIdentificationObject.ProductName:
|
||||||
{
|
{
|
||||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName);
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName ?? "");
|
||||||
result.Add((byte)bytes.Length);
|
result.Add((byte)bytes.Length);
|
||||||
result.AddRange(bytes);
|
result.AddRange(bytes);
|
||||||
}
|
}
|
||||||
@@ -817,7 +806,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
case ModbusDeviceIdentificationObject.ModelName:
|
case ModbusDeviceIdentificationObject.ModelName:
|
||||||
{
|
{
|
||||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName);
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName ?? "");
|
||||||
result.Add((byte)bytes.Length);
|
result.Add((byte)bytes.Length);
|
||||||
result.AddRange(bytes);
|
result.AddRange(bytes);
|
||||||
}
|
}
|
||||||
@@ -825,7 +814,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
case ModbusDeviceIdentificationObject.UserApplicationName:
|
case ModbusDeviceIdentificationObject.UserApplicationName:
|
||||||
{
|
{
|
||||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName);
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName ?? "");
|
||||||
result.Add((byte)bytes.Length);
|
result.Add((byte)bytes.Length);
|
||||||
result.AddRange(bytes);
|
result.AddRange(bytes);
|
||||||
}
|
}
|
||||||
@@ -833,9 +822,8 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
if (deviceIdentification.ExtendedObjects.ContainsKey(objectId))
|
if (deviceIdentification.ExtendedObjects.TryGetValue(objectId, out byte[] bytes))
|
||||||
{
|
{
|
||||||
byte[] bytes = deviceIdentification.ExtendedObjects[objectId];
|
|
||||||
result.Add((byte)bytes.Length);
|
result.Add((byte)bytes.Length);
|
||||||
result.AddRange(bytes);
|
result.AddRange(bytes);
|
||||||
}
|
}
|
||||||
@@ -850,6 +838,29 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
return [.. result];
|
return [.. result];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static byte[] ReturnResponse(List<byte> response)
|
||||||
|
{
|
||||||
|
ushort followingBytes = (ushort)(response.Count - 6);
|
||||||
|
var bytes = followingBytes.ToBigEndianBytes();
|
||||||
|
response[4] = bytes[0];
|
||||||
|
response[5] = bytes[1];
|
||||||
|
|
||||||
|
return [.. response];
|
||||||
|
}
|
||||||
|
|
||||||
#endregion Request Handling
|
#endregion Request Handling
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
sb.AppendLine($"TCP Proxy");
|
||||||
|
sb.AppendLine($" {nameof(ListenAddress)}: {ListenAddress}");
|
||||||
|
sb.AppendLine($" {nameof(ListenPort)}: {ListenPort}");
|
||||||
|
sb.AppendLine($" {nameof(Client)}: {Client.GetType().Name}");
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
95
src/AMWD.Protocols.Modbus.Tcp/ModbusTcpServer.cs
Normal file
95
src/AMWD.Protocols.Modbus.Tcp/ModbusTcpServer.cs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using AMWD.Protocols.Modbus.Common;
|
||||||
|
using AMWD.Protocols.Modbus.Common.Events;
|
||||||
|
using AMWD.Protocols.Modbus.Common.Utils;
|
||||||
|
|
||||||
|
namespace AMWD.Protocols.Modbus.Tcp
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implements a Modbus TCP server proxying all requests to a virtual Modbus client.
|
||||||
|
/// </summary>
|
||||||
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
|
public class ModbusTcpServer : ModbusTcpProxy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ModbusTcpServer"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="listenAddress">The <see cref="IPAddress"/> to listen on.</param>
|
||||||
|
public ModbusTcpServer(IPAddress listenAddress)
|
||||||
|
: base(new VirtualModbusClient(), listenAddress)
|
||||||
|
{
|
||||||
|
TypedClient.CoilWritten += (sender, e) => CoilWritten?.Invoke(this, e);
|
||||||
|
TypedClient.RegisterWritten += (sender, e) => RegisterWritten?.Invoke(this, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Events
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that a <see cref="Coil"/>-value received through a remote client has been written.
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<CoilWrittenEventArgs> CoilWritten;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that a <see cref="HoldingRegister"/>-value received from a remote client has been written.
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<RegisterWrittenEventArgs> RegisterWritten;
|
||||||
|
|
||||||
|
#endregion Events
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
internal VirtualModbusClient TypedClient
|
||||||
|
=> Client as VirtualModbusClient;
|
||||||
|
|
||||||
|
#endregion Properties
|
||||||
|
|
||||||
|
#region Device Handling
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.AddDevice(byte)"/>
|
||||||
|
public bool AddDevice(byte unitId)
|
||||||
|
=> TypedClient.AddDevice(unitId);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.RemoveDevice(byte)"/>
|
||||||
|
public bool RemoveDevice(byte unitId)
|
||||||
|
=> TypedClient.RemoveDevice(unitId);
|
||||||
|
|
||||||
|
#endregion Device Handling
|
||||||
|
|
||||||
|
#region Entity Handling
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.GetCoil(byte, ushort)"/>
|
||||||
|
public Coil GetCoil(byte unitId, ushort address)
|
||||||
|
=> TypedClient.GetCoil(unitId, address);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.SetCoil(byte, Coil)"/>
|
||||||
|
public void SetCoil(byte unitId, Coil coil)
|
||||||
|
=> TypedClient.SetCoil(unitId, coil);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.GetDiscreteInput(byte, ushort)"/>
|
||||||
|
public DiscreteInput GetDiscreteInput(byte unitId, ushort address)
|
||||||
|
=> TypedClient.GetDiscreteInput(unitId, address);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.SetDiscreteInput(byte, DiscreteInput)"/>
|
||||||
|
public void SetDiscreteInput(byte unitId, DiscreteInput discreteInput)
|
||||||
|
=> TypedClient.SetDiscreteInput(unitId, discreteInput);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.GetHoldingRegister(byte, ushort)"/>
|
||||||
|
public HoldingRegister GetHoldingRegister(byte unitId, ushort address)
|
||||||
|
=> TypedClient.GetHoldingRegister(unitId, address);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.SetHoldingRegister(byte, HoldingRegister)"/>
|
||||||
|
public void SetHoldingRegister(byte unitId, HoldingRegister holdingRegister)
|
||||||
|
=> TypedClient.SetHoldingRegister(unitId, holdingRegister);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.GetInputRegister(byte, ushort)"/>
|
||||||
|
public InputRegister GetInputRegister(byte unitId, ushort address)
|
||||||
|
=> TypedClient.GetInputRegister(unitId, address);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.SetInputRegister(byte, InputRegister)"/>
|
||||||
|
public void SetInputRegister(byte unitId, InputRegister inputRegister)
|
||||||
|
=> TypedClient.SetInputRegister(unitId, inputRegister);
|
||||||
|
|
||||||
|
#endregion Entity Handling
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,14 +23,14 @@ float voltage = registers.GetSingle();
|
|||||||
Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V");
|
Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V");
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to use the `RTU over TCP` protocol instead, you can do this on initialization:
|
If you have a device speaking `RTU` connected over `TCP`, you can use it as followed:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// [...]
|
// [...]
|
||||||
|
|
||||||
using var client = new ModbusTcpClient(host, port)
|
using var client = new ModbusTcpClient(host, port)
|
||||||
{
|
{
|
||||||
Protocol = new RtuOverTcpProtocol();
|
Protocol = new RtuProtocol()
|
||||||
};
|
};
|
||||||
|
|
||||||
// [...]
|
// [...]
|
||||||
@@ -44,10 +44,10 @@ using var client = new ModbusTcpClient(host, port)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Published under MIT License (see [**tl;dr**Legal])
|
Published under MIT License (see [choose a license])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[v1.1b3]: https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf
|
[v1.1b3]: https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf
|
||||||
[v1.0b]: https://modbus.org/docs/Modbus_Messaging_Implementation_Guide_V1_0b.pdf
|
[v1.0b]: https://modbus.org/docs/Modbus_Messaging_Implementation_Guide_V1_0b.pdf
|
||||||
[**tl;dr**Legal]: https://www.tldrlegal.com/license/mit-license
|
[choose a license]: https://choosealicense.com/licenses/mit/
|
||||||
30
src/AMWD.Protocols.Modbus.Tcp/Utils/IPEndPointWrapper.cs
Normal file
30
src/AMWD.Protocols.Modbus.Tcp/Utils/IPEndPointWrapper.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
|
||||||
|
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||||
|
{
|
||||||
|
/// <inheritdoc cref="IPEndPoint" />
|
||||||
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
|
internal class IPEndPointWrapper(EndPoint endPoint)
|
||||||
|
{
|
||||||
|
private readonly IPEndPoint _ipEndPoint = (IPEndPoint)endPoint;
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IPEndPoint.Address"/>
|
||||||
|
public virtual IPAddress Address
|
||||||
|
{
|
||||||
|
get => _ipEndPoint.Address;
|
||||||
|
set => _ipEndPoint.Address = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IPEndPoint.Port"/>
|
||||||
|
public virtual int Port
|
||||||
|
{
|
||||||
|
get => _ipEndPoint.Port;
|
||||||
|
set => _ipEndPoint.Port = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Properties
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,18 +8,9 @@ namespace AMWD.Protocols.Modbus.Tcp.Utils
|
|||||||
{
|
{
|
||||||
/// <inheritdoc cref="NetworkStream" />
|
/// <inheritdoc cref="NetworkStream" />
|
||||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
internal class NetworkStreamWrapper : IDisposable
|
internal class NetworkStreamWrapper(NetworkStream stream) : IDisposable
|
||||||
{
|
{
|
||||||
private readonly NetworkStream _stream;
|
private readonly NetworkStream _stream = stream;
|
||||||
|
|
||||||
[Obsolete("Constructor only for mocking on UnitTests!", error: true)]
|
|
||||||
public NetworkStreamWrapper()
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public NetworkStreamWrapper(NetworkStream stream)
|
|
||||||
{
|
|
||||||
_stream = stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="NetworkStream.Dispose" />
|
/// <inheritdoc cref="NetworkStream.Dispose" />
|
||||||
public virtual void Dispose()
|
public virtual void Dispose()
|
||||||
26
src/AMWD.Protocols.Modbus.Tcp/Utils/SocketWrapper.cs
Normal file
26
src/AMWD.Protocols.Modbus.Tcp/Utils/SocketWrapper.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
|
||||||
|
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||||
|
{
|
||||||
|
/// <inheritdoc cref="Socket" />
|
||||||
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
|
internal class SocketWrapper(Socket socket) : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Socket _socket = socket;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Socket.DualMode" />
|
||||||
|
public virtual bool DualMode
|
||||||
|
{
|
||||||
|
get => _socket.DualMode;
|
||||||
|
set => _socket.DualMode = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Socket.IsBound" />
|
||||||
|
public virtual bool IsBound
|
||||||
|
=> _socket.IsBound;
|
||||||
|
|
||||||
|
public virtual void Dispose()
|
||||||
|
=> _socket.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,19 +3,30 @@ using System.Net;
|
|||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Transactions;
|
||||||
|
|
||||||
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||||
{
|
{
|
||||||
/// <inheritdoc cref="TcpClient" />
|
/// <inheritdoc cref="TcpClient" />
|
||||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
internal class TcpClientWrapper(AddressFamily addressFamily) : IDisposable
|
internal class TcpClientWrapper : IDisposable
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
|
|
||||||
private readonly TcpClient _client = new(addressFamily);
|
private readonly TcpClient _client;
|
||||||
|
|
||||||
#endregion Fields
|
#endregion Fields
|
||||||
|
|
||||||
|
public TcpClientWrapper(AddressFamily addressFamily)
|
||||||
|
{
|
||||||
|
_client = new TcpClient(addressFamily);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TcpClientWrapper(TcpClient client)
|
||||||
|
{
|
||||||
|
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
}
|
||||||
|
|
||||||
#region Properties
|
#region Properties
|
||||||
|
|
||||||
/// <inheritdoc cref="TcpClient.Connected" />
|
/// <inheritdoc cref="TcpClient.Connected" />
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
87
src/AMWD.Protocols.Modbus.Tcp/Utils/TcpListenerWrapper.cs
Normal file
87
src/AMWD.Protocols.Modbus.Tcp/Utils/TcpListenerWrapper.cs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||||
|
{
|
||||||
|
/// <inheritdoc cref="TcpListener" />
|
||||||
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
|
internal class TcpListenerWrapper(IPAddress localaddr, int port) : IDisposable
|
||||||
|
{
|
||||||
|
#region Fields
|
||||||
|
|
||||||
|
private readonly TcpListener _tcpListener = new(localaddr, port);
|
||||||
|
|
||||||
|
#endregion Fields
|
||||||
|
|
||||||
|
#region Constructor
|
||||||
|
|
||||||
|
#endregion Constructor
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
/// <inheritdoc cref="TcpListener.LocalEndpoint"/>
|
||||||
|
public virtual IPEndPointWrapper LocalIPEndPoint
|
||||||
|
=> new(_tcpListener.LocalEndpoint);
|
||||||
|
|
||||||
|
public virtual SocketWrapper Socket
|
||||||
|
=> new(_tcpListener.Server);
|
||||||
|
|
||||||
|
#endregion Properties
|
||||||
|
|
||||||
|
#region Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Accepts a pending connection request as a cancellable asynchronous operation.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// This operation will not block. The returned <see cref="Task{TResult}"/> object will complete after the TCP connection has been accepted.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Use the <see cref="TcpClientWrapper.GetStream"/> method to obtain the underlying <see cref="NetworkStreamWrapper"/> of the returned <see cref="TcpClientWrapper"/> in the <see cref="Task{TResult}"/>.
|
||||||
|
/// The <see cref="NetworkStreamWrapper"/> will provide you with methods for sending and receiving with the remote host.
|
||||||
|
/// When you are through with the <see cref="TcpClientWrapper"/>, be sure to call its <see cref="TcpClientWrapper.Close"/> method.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The task object representing the asynchronous operation.
|
||||||
|
/// The <see cref="Task{TResult}.Result"/> property on the task object returns a <see cref="TcpClientWrapper"/> used to send and receive data.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="InvalidOperationException">The listener has not been started with a call to <see cref="Start"/>.</exception>
|
||||||
|
/// <exception cref="SocketException">
|
||||||
|
/// Use the <see cref="SocketException.ErrorCode"/> property to obtain the specific error code.
|
||||||
|
/// When you have obtained this code, you can refer to the
|
||||||
|
/// <see href="https://learn.microsoft.com/en-us/windows/desktop/winsock/windows-sockets-error-codes-2">Windows Sockets version 2 API error code</see>
|
||||||
|
/// documentation for a detailed description of the error.
|
||||||
|
/// </exception>
|
||||||
|
/// <exception cref="OperationCanceledException">The cancellation token was canceled. This exception is stored into the returned task.</exception>
|
||||||
|
public virtual async Task<TcpClientWrapper> AcceptTcpClientAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
var tcpClient = await _tcpListener.AcceptTcpClientAsync(cancellationToken);
|
||||||
|
#else
|
||||||
|
var tcpClient = await _tcpListener.AcceptTcpClientAsync();
|
||||||
|
#endif
|
||||||
|
return new TcpClientWrapper(tcpClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Start()
|
||||||
|
=> _tcpListener.Start();
|
||||||
|
|
||||||
|
public virtual void Stop()
|
||||||
|
=> _tcpListener.Stop();
|
||||||
|
|
||||||
|
public virtual void Dispose()
|
||||||
|
{
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
_tcpListener.Dispose();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Methods
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/Directory.Build.props
Normal file
51
src/Directory.Build.props
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
@@ -32,29 +32,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowNullForGetByte()
|
public void ShouldThrowNullForGetByte()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
HoldingRegister register = null;
|
HoldingRegister register = null;
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
register.GetByte();
|
Assert.ThrowsExactly<ArgumentNullException>(() => register.GetByte());
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentForGetByte()
|
public void ShouldThrowArgumentForGetByte()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var obj = new Coil();
|
var obj = new Coil();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
obj.GetByte();
|
Assert.ThrowsExactly<ArgumentException>(() => obj.GetByte());
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -84,29 +78,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowNullForGetUInt16()
|
public void ShouldThrowNullForGetUInt16()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
HoldingRegister register = null;
|
HoldingRegister register = null;
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
register.GetUInt16();
|
Assert.ThrowsExactly<ArgumentNullException>(() => register.GetUInt16());
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentForGetUInt16()
|
public void ShouldThrowArgumentForGetUInt16()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var obj = new Coil();
|
var obj = new Coil();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
obj.GetUInt16();
|
Assert.ThrowsExactly<ArgumentException>(() => obj.GetUInt16());
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -145,21 +133,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowNullOnGetUInt32()
|
public void ShouldThrowNullOnGetUInt32()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
HoldingRegister[] registers = null;
|
HoldingRegister[] registers = null;
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetUInt32(0);
|
Assert.ThrowsExactly<ArgumentNullException>(() => registers.GetUInt32(0));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentOnGetUInt32ForLength()
|
public void ShouldThrowArgumentOnGetUInt32ForLength()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -168,16 +151,13 @@
|
|||||||
new() { Address = 101, HighByte = 0x01, LowByte = 0x02 }
|
new() { Address = 101, HighByte = 0x01, LowByte = 0x02 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetUInt32(0);
|
Assert.ThrowsExactly<ArgumentException>(() => registers.GetUInt32(1));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[TestMethod]
|
||||||
[DataRow(1)]
|
[DataRow(1)]
|
||||||
[DataRow(-1)]
|
[DataRow(-1)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowArgumentOutOfRangeOnGetUInt32(int startIndex)
|
public void ShouldThrowArgumentOutOfRangeOnGetUInt32(int startIndex)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -187,14 +167,11 @@
|
|||||||
new() { Address = 100, HighByte = 0x03, LowByte = 0x04 }
|
new() { Address = 100, HighByte = 0x03, LowByte = 0x04 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetUInt32(startIndex);
|
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => registers.GetUInt32(startIndex));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentOnGetUInt32ForType()
|
public void ShouldThrowArgumentOnGetUInt32ForType()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -204,10 +181,8 @@
|
|||||||
new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 }
|
new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetUInt32(0);
|
Assert.ThrowsExactly<ArgumentException>(() => registers.GetUInt32(0));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -250,21 +225,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowNullOnGetUInt64()
|
public void ShouldThrowNullOnGetUInt64()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
HoldingRegister[] registers = null;
|
HoldingRegister[] registers = null;
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetUInt64(0);
|
Assert.ThrowsExactly<ArgumentNullException>(() => registers.GetUInt64(0));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentOnGetUInt64ForLength()
|
public void ShouldThrowArgumentOnGetUInt64ForLength()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -275,16 +245,13 @@
|
|||||||
new() { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
new() { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetUInt64(0);
|
Assert.ThrowsExactly<ArgumentException>(() => registers.GetUInt64(0));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[TestMethod]
|
||||||
[DataRow(1)]
|
[DataRow(1)]
|
||||||
[DataRow(-1)]
|
[DataRow(-1)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowArgumentOutOfRangeOnGetUInt64(int startIndex)
|
public void ShouldThrowArgumentOutOfRangeOnGetUInt64(int startIndex)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -296,14 +263,11 @@
|
|||||||
new() { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
new() { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetUInt64(startIndex);
|
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => registers.GetUInt64(startIndex));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentOnGetUInt64ForType()
|
public void ShouldThrowArgumentOnGetUInt64ForType()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -315,10 +279,8 @@
|
|||||||
new InputRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
new InputRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetUInt64(0);
|
Assert.ThrowsExactly<ArgumentException>(() => registers.GetUInt64(0));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Modbus to value
|
#endregion Modbus to value
|
||||||
@@ -368,7 +330,7 @@
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsNotNull(registers);
|
Assert.IsNotNull(registers);
|
||||||
Assert.AreEqual(2, registers.Count);
|
Assert.HasCount(2, registers);
|
||||||
|
|
||||||
Assert.AreEqual(5, registers[0].Address);
|
Assert.AreEqual(5, registers[0].Address);
|
||||||
Assert.AreEqual(0x00, registers[0].HighByte);
|
Assert.AreEqual(0x00, registers[0].HighByte);
|
||||||
@@ -390,7 +352,7 @@
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsNotNull(registers);
|
Assert.IsNotNull(registers);
|
||||||
Assert.AreEqual(2, registers.Count);
|
Assert.HasCount(2, registers);
|
||||||
|
|
||||||
Assert.AreEqual(6, registers[0].Address);
|
Assert.AreEqual(6, registers[0].Address);
|
||||||
Assert.AreEqual(0x00, registers[0].HighByte);
|
Assert.AreEqual(0x00, registers[0].HighByte);
|
||||||
@@ -412,7 +374,7 @@
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsNotNull(registers);
|
Assert.IsNotNull(registers);
|
||||||
Assert.AreEqual(4, registers.Count);
|
Assert.HasCount(4, registers);
|
||||||
|
|
||||||
Assert.AreEqual(10, registers[0].Address);
|
Assert.AreEqual(10, registers[0].Address);
|
||||||
Assert.AreEqual(0x00, registers[0].HighByte);
|
Assert.AreEqual(0x00, registers[0].HighByte);
|
||||||
@@ -442,7 +404,7 @@
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsNotNull(registers);
|
Assert.IsNotNull(registers);
|
||||||
Assert.AreEqual(4, registers.Count);
|
Assert.HasCount(4, registers);
|
||||||
|
|
||||||
Assert.AreEqual(13, registers[0].Address);
|
Assert.AreEqual(13, registers[0].Address);
|
||||||
Assert.AreEqual(0x00, registers[0].HighByte);
|
Assert.AreEqual(0x00, registers[0].HighByte);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user