35 Commits

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

View File

@@ -91,6 +91,7 @@ dotnet_naming_style.prefix_underscore.required_prefix = _
[*.cs]
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
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}]
indent_size = 2
indent_style = space
[*.{json,yml}]
end_of_line = lf

3
.gitignore vendored
View File

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

View File

@@ -20,17 +20,11 @@ build-debug:
rules:
- if: $CI_COMMIT_TAG == null
script:
- dotnet restore --no-cache --force
- dotnet build -c Debug --nologo --no-restore --no-incremental
- shopt -s globstar
- 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/
- dotnet build -c Debug --nologo
- mv ./**/*.nupkg ./artifacts/
- mv ./**/*.snupkg ./artifacts/
artifacts:
paths:
- artifacts/*.nupkg
@@ -47,10 +41,20 @@ test-debug:
- 64bit
rules:
- if: $CI_COMMIT_TAG == null
coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/'
coverage: /Branch coverage[\s\S].+%/
before_script:
- dotnet tool install dotnet-reportgenerator-globaltool --tool-path /dotnet-tools
script:
- dotnet restore --no-cache --force
- dotnet test -c Debug --nologo --no-restore
- dotnet test -c Debug --nologo /p:CoverletOutputFormat=Cobertura
- /dotnet-tools/reportgenerator "-reports:${CI_PROJECT_DIR}/**/coverage.cobertura.xml" "-targetdir:/reports" -reportType:TextSummary
after_script:
- cat /reports/Summary.txt
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: ./**/coverage.cobertura.xml
deploy-debug:
stage: deploy
@@ -60,7 +64,7 @@ deploy-debug:
tags:
- docker
- lnx
- 64bit
- server
rules:
- if: $CI_COMMIT_TAG == null
script:
@@ -77,22 +81,16 @@ build-release:
rules:
- if: $CI_COMMIT_TAG != null
script:
- dotnet restore --no-cache --force
- dotnet build -c Release --nologo --no-restore --no-incremental
- shopt -s globstar
- 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/
- dotnet build -c Release --nologo
- mv ./**/*.nupkg ./artifacts/
- mv ./**/*.snupkg ./artifacts/
artifacts:
paths:
- artifacts/*.nupkg
- artifacts/*.snupkg
expire_in: 1 days
expire_in: 7 days
test-release:
stage: test
@@ -104,10 +102,19 @@ test-release:
- amd64
rules:
- if: $CI_COMMIT_TAG != null
coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/'
before_script:
- dotnet tool install dotnet-reportgenerator-globaltool --tool-path /dotnet-tools
script:
- dotnet restore --no-cache --force
- dotnet test -c Release --nologo --no-restore
- dotnet test -c Release --nologo /p:CoverletOutputFormat=Cobertura
- /dotnet-tools/reportgenerator "-reports:${CI_PROJECT_DIR}/**/coverage.cobertura.xml" "-targetdir:/reports" -reportType:TextSummary
after_script:
- cat /reports/Summary.txt
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: ./**/coverage.cobertura.xml
deploy-release:
stage: deploy
@@ -117,8 +124,34 @@ deploy-release:
tags:
- docker
- lnx
- 64bit
- server
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
publish-docs:
variables:
DOCFX_SOURCE_REPOSITORY_URL: "https://github.com/AM-WD/AMWD.Protocols.Modbus"
stage: deploy
tags:
- docker
- lnx
- server
rules:
- if: $CI_COMMIT_TAG != null
before_script:
- apt-get update
- apt-get -y install zip unzip
- dotnet tool install docfx --tool-path /dotnet-tools
script:
# Build the docs
- dotnet build -c Release --nologo
- /dotnet-tools/docfx metadata docs/docfx.json
- /dotnet-tools/docfx build docs/docfx.json
# Deploy the docs
- cd docs/_site
- zip -r ../docs.zip *
- curl --user "$DOCS_DEPLOY_USER:$DOCS_DEPLOY_PASS" -F docs=modbus -F dump=@../docs.zip "$DOCS_DEPLOY_URL"

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ 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}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Common", "src\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
@@ -29,13 +29,30 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{C8065AE3
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}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Tests", "test\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}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Tcp", "src\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}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Serial", "src\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}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliClient", "tool\CliClient\CliClient.csproj", "{B0E53462-B0ED-4685-8AA5-948DC160EE27}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliProxy", "tool\CliProxy\CliProxy.csproj", "{AC922E80-E9B6-493D-B1D1-752527E883ED}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
ProjectSection(SolutionItems) = preProject
src\Directory.Build.props = src\Directory.Build.props
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0C2CC421-9808-4CA2-BEA8-11493467DBCE}"
ProjectSection(SolutionItems) = preProject
test\Directory.Build.props = test\Directory.Build.props
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tool", "tool", "{3429CE19-211E-4AFA-9629-D7E1A360B7AC}"
ProjectSection(SolutionItems) = preProject
tool\Directory.Build.props = tool\Directory.Build.props
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -59,18 +76,28 @@ Global
{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
{B0E53462-B0ED-4685-8AA5-948DC160EE27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B0E53462-B0ED-4685-8AA5-948DC160EE27}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B0E53462-B0ED-4685-8AA5-948DC160EE27}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B0E53462-B0ED-4685-8AA5-948DC160EE27}.Release|Any CPU.Build.0 = Release|Any CPU
{AC922E80-E9B6-493D-B1D1-752527E883ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AC922E80-E9B6-493D-B1D1-752527E883ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AC922E80-E9B6-493D-B1D1-752527E883ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AC922E80-E9B6-493D-B1D1-752527E883ED}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{2B7689D8-9E56-4DEB-B40E-F70DB4A6F250} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{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}
{146070C4-E922-4F5A-AD6F-9A899186E26E} = {0C2CC421-9808-4CA2-BEA8-11493467DBCE}
{8C888A84-CD09-4087-B5DA-67708ABBABA2} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{D966826F-EE6C-4BC0-9185-C2A9A50FD586} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{B0E53462-B0ED-4685-8AA5-948DC160EE27} = {3429CE19-211E-4AFA-9629-D7E1A360B7AC}
{AC922E80-E9B6-493D-B1D1-752527E883ED} = {3429CE19-211E-4AFA-9629-D7E1A360B7AC}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E4FD8EF0-3594-4994-BE80-5FADA5EE17B4}

BIN
AMWD.Protocols.Modbus.snk Normal file

Binary file not shown.

View File

@@ -2,33 +2,97 @@
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).
## [Unreleased]
_no changes_
### Added
- New automatic documentation generation using docfx.
- Additional articles for the documentation.
### Changed
- Reorganized folder structure to allow documentation generation.
## [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)
### 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
- Renamed `ModbusSerialServer` to `ModbusRtuServer` to clearify the protocol that is used
- Made `Protocol` property of `ModbusClientBase` non-abstract
- Renamed `ModbusSerialServer` to `ModbusRtuServer` to clearify the protocol that is used.
- Made `Protocol` property of `ModbusClientBase` non-abstract.
### 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)
First "final" re-implementation
First "final" re-implementation.
## v0.1.0 (2022-08-28)
@@ -38,6 +102,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.2.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/tree/v0.2.0

View File

@@ -1,50 +1,20 @@
<Project>
<PropertyGroup>
<LangVersion>12.0</LangVersion>
<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>
<Company>AM.WD</Company>
<Authors>Andreas Müller</Authors>
<Copyright>© {copyright:2018-} AM.WD</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<SignAssembly>true</SignAssembly>
<PublicKey>0024000004800000940000000602000000240000525341310004000001000100adcc4f9f5bb3ac73cb30661f6f35772b8f90a74412925764a960af06ef125bdcec05ed1d139503d5203fb72aa3fa74bab58e82ac2a6cd4b650f8cbf7086a71bc2dfc67e95b8d26d776d60856acf3121f831529b1a4dee91b34ac84f95f71a1165b7783edb591929ba2a684100c92bbed8859c7266fb507f6f55bb6f7fcac80b4</PublicKey>
<MoqPublicKey>0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7</MoqPublicKey>
</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>
<None Include="../package-icon.png" Pack="true" PackagePath="/" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Unclassified.NetRevisionTask" Version="0.4.3">
<PackageReference Include="AMWD.NetRevisionTask" Version="1.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -1,4 +1,4 @@
The MIT License
MIT License
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
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
The above copyright notice and this permission notice (including the next
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
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

View File

@@ -2,9 +2,12 @@
Here you can find a basic implementation of the Modbus protocol.
![NuGet Version](https://shields.io/nuget/v/AMWD.Protocols.Modbus.Common?style=flat&logo=nuget)
![Test Coverage](https://git.am-wd.de/am-wd/amwd.protocols.modbus/badges/main/coverage.svg?style=flat)
## Overview
The project is divided into four parts.
The project is divided into multiple parts.
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]).
@@ -20,11 +23,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.
### [Proxy]
The package contains a TCP and a RTU server implementation as proxy which contains a client of your choice to connect to.
### [Serial]
This package contains some wrappers and implementations for the serial protocol.
@@ -39,16 +37,14 @@ It uses a specific TCP connection implementation and plugs all things from the C
---
Published under [MIT License] (see [**tl;dr**Legal])
[![Buy me a Coffee](https://shields.am-wd.de/badge/PayPal-Buy_me_a_Coffee-yellow?style=flat&logo=paypal)](https://link.am-wd.de/donate)
[![built with Codeium](https://codeium.com/badges/main)](https://link.am-wd.de/codeium)
Published under [MIT License] (see [choose a license])
[![Buy me a Coffee](https://shields.io/badge/PayPal-Buy_me_a_Coffee-yellow?style=flat&logo=paypal)](https://link.am-wd.de/donate)
[see here]: https://github.com/andreasAMmueller/Modbus
[Common]: AMWD.Protocols.Modbus.Common/README.md
[Proxy]: AMWD.Protocols.Modbus.Proxy/README.md
[Serial]: AMWD.Protocols.Modbus.Serial/README.md
[TCP]: AMWD.Protocols.Modbus.Tcp/README.md
[Common]: src/AMWD.Protocols.Modbus.Common/README.md
[Serial]: src/AMWD.Protocols.Modbus.Serial/README.md
[TCP]: src/AMWD.Protocols.Modbus.Tcp/README.md
[MIT License]: LICENSE.txt
[**tl;dr**Legal]: https://www.tldrlegal.com/license/mit-license
[choose a license]: https://choosealicense.com/licenses/mit/

View File

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

View File

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

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

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

63
docs/docfx.json Normal file
View File

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

BIN
docs/images/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

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

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

After

Width:  |  Height:  |  Size: 13 KiB

26
docs/index.md Normal file
View File

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

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

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

4
docs/toc.yml Normal file
View File

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

View File

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

View File

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

View File

@@ -1,28 +1,36 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace AMWD.Protocols.Modbus.Common.Contracts
{
/// <summary>
/// Base implementation of a Modbus client.
/// </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;
/// <summary>
/// Gets or sets a value indicating whether the connection should be disposed of by <see cref="Dispose()"/>.
/// </summary>
protected readonly bool disposeConnection;
protected readonly bool disposeConnection = disposeConnection;
/// <summary>
/// Gets or sets the <see cref="IModbusConnection"/> responsible for invoking the requests.
/// </summary>
protected readonly IModbusConnection connection;
protected readonly IModbusConnection connection = connection ?? throw new ArgumentNullException(nameof(connection));
/// <summary>
/// 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)
{ }
/// <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>
/// Gets or sets the protocol type to use.
/// </summary>
@@ -67,7 +61,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
Assertions();
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);
// The protocol processes complete bytes from the response.
@@ -92,7 +86,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
Assertions();
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);
// The protocol processes complete bytes from the response.
@@ -117,7 +111,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
Assertions();
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);
var holdingRegisters = Protocol.DeserializeReadHoldingRegisters(response).ToList();
@@ -140,7 +134,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
Assertions();
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);
var inputRegisters = Protocol.DeserializeReadInputRegisters(response).ToList();
@@ -184,7 +178,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
do
{
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);
result = Protocol.DeserializeReadDeviceIdentification(response);
@@ -247,7 +241,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
Assertions();
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);
var result = Protocol.DeserializeWriteSingleCoil(response);
@@ -268,7 +262,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
Assertions();
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);
var result = Protocol.DeserializeWriteSingleHoldingRegister(response);
@@ -289,7 +283,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
Assertions();
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);
var (firstAddress, count) = Protocol.DeserializeWriteMultipleCoils(response);
@@ -309,7 +303,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
Assertions();
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);
var (firstAddress, count) = Protocol.DeserializeWriteMultipleHoldingRegisters(response);

View File

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

View File

@@ -8,29 +8,39 @@ namespace AMWD.Protocols.Modbus.Common.Events
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
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>
/// Gets or sets the unit id.
/// </summary>
public byte UnitId { get; set; }
public byte UnitId { get; }
/// <summary>
/// Gets or sets the address of the register.
/// </summary>
public ushort Address { get; set; }
public ushort Address { get; }
/// <summary>
/// Gets or sets the value of the register.
/// </summary>
public ushort Value { get; set; }
public ushort Value { get; }
/// <summary>
/// Gets or sets the high byte of the register.
/// </summary>
public byte HighByte { get; set; }
public byte HighByte { get; }
/// <summary>
/// Gets or sets the low byte of the register.
/// </summary>
public byte LowByte { get; set; }
public byte LowByte { get; }
}
}

View File

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

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace AMWD.Protocols.Modbus.Common
@@ -12,14 +13,14 @@ namespace AMWD.Protocols.Modbus.Common
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();
b.SwapBigEndian();
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);
b.SwapBigEndian();

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Text;
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.
/// </summary>
public bool IsIndividualAccessAllowed { get; set; }
/// <inheritdoc/>
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine(nameof(DeviceIdentification));
sb.AppendLine($" {nameof(VendorName)}: {VendorName}");
sb.AppendLine($" {nameof(ProductCode)}: {ProductCode}");
sb.AppendLine($" {nameof(MajorMinorRevision)}: {MajorMinorRevision}");
sb.AppendLine($" {nameof(VendorUrl)}: {VendorUrl}");
sb.AppendLine($" {nameof(ProductName)}: {ProductName}");
sb.AppendLine($" {nameof(ModelName)}: {ModelName}");
sb.AppendLine($" {nameof(UserApplicationName)}: {UserApplicationName}");
sb.AppendLine($" {nameof(IsIndividualAccessAllowed)}: {IsIndividualAccessAllowed}");
return sb.ToString();
}
}
}

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ namespace AMWD.Protocols.Modbus.Common.Models
/// Initializes a new instance of the <see cref="ModbusDevice"/> class.
/// </remarks>
/// <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 _rwLockDiscreteInputs = new();

View File

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

View File

@@ -92,11 +92,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadCoils:X2}";
// Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes();
var addrBytes = startAddress.ToBigEndianBytes();
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
// Quantity
byte[] countBytes = count.ToBigEndianBytes();
var countBytes = count.ToBigEndianBytes();
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
// LRC
@@ -151,11 +151,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadDiscreteInputs:X2}";
// Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes();
var addrBytes = startAddress.ToBigEndianBytes();
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
// Quantity
byte[] countBytes = count.ToBigEndianBytes();
var countBytes = count.ToBigEndianBytes();
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
// LRC
@@ -209,11 +209,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadHoldingRegisters:X2}";
// Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes();
var addrBytes = startAddress.ToBigEndianBytes();
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
// Quantity
byte[] countBytes = count.ToBigEndianBytes();
var countBytes = count.ToBigEndianBytes();
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
// LRC
@@ -264,11 +264,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadInputRegisters:X2}";
// Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes();
var addrBytes = startAddress.ToBigEndianBytes();
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
// Quantity
byte[] countBytes = count.ToBigEndianBytes();
var countBytes = count.ToBigEndianBytes();
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
// LRC
@@ -383,7 +383,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteSingleCoil:X2}";
// Starting address
byte[] addrBytes = coil.Address.ToBigEndianBytes();
var addrBytes = coil.Address.ToBigEndianBytes();
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
// Value
@@ -426,7 +426,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteSingleRegister:X2}";
// Starting address
byte[] addrBytes = register.Address.ToBigEndianBytes();
var addrBytes = register.Address.ToBigEndianBytes();
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
// Value
@@ -497,11 +497,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteMultipleCoils:X2}";
// Starting address
byte[] addrBytes = firstAddress.ToBigEndianBytes();
var addrBytes = firstAddress.ToBigEndianBytes();
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
// Quantity
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
// Byte count
@@ -567,11 +567,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteMultipleRegisters:X2}";
// Starting address
byte[] addrBytes = firstAddress.ToBigEndianBytes();
var addrBytes = firstAddress.ToBigEndianBytes();
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
// Quantity
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
// Byte count
@@ -675,6 +675,10 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
/// <summary>
/// Calculate LRC for Modbus ASCII.
/// </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="start">The start index.</param>
/// <param name="length">The number of bytes to calculate.</param>

View File

@@ -6,11 +6,15 @@ using AMWD.Protocols.Modbus.Common.Contracts;
namespace AMWD.Protocols.Modbus.Common.Protocols
{
/// <summary>
/// Implementation of the Modbus RTU over TCP protocol.
/// Implementation of the Modbus RTU over Modbus TCP protocol.
/// </summary>
/// <remarks>
/// 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>
public class RtuOverTcpProtocol : IModbusProtocol
{
@@ -115,12 +119,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.ReadCoils;
// Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes();
var addrBytes = startAddress.ToBigEndianBytes();
request[8] = addrBytes[0];
request[9] = addrBytes[1];
// Quantity
byte[] countBytes = count.ToBigEndianBytes();
var countBytes = count.ToBigEndianBytes();
request[10] = countBytes[0];
request[11] = countBytes[1];
@@ -178,12 +182,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
// Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes();
var addrBytes = startAddress.ToBigEndianBytes();
request[8] = addrBytes[0];
request[9] = addrBytes[1];
// Quantity
byte[] countBytes = count.ToBigEndianBytes();
var countBytes = count.ToBigEndianBytes();
request[10] = countBytes[0];
request[11] = countBytes[1];
@@ -241,12 +245,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
// Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes();
var addrBytes = startAddress.ToBigEndianBytes();
request[8] = addrBytes[0];
request[9] = addrBytes[1];
// Quantity
byte[] countBytes = count.ToBigEndianBytes();
var countBytes = count.ToBigEndianBytes();
request[10] = countBytes[0];
request[11] = countBytes[1];
@@ -301,12 +305,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.ReadInputRegisters;
// Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes();
var addrBytes = startAddress.ToBigEndianBytes();
request[8] = addrBytes[0];
request[9] = addrBytes[1];
// Quantity
byte[] countBytes = count.ToBigEndianBytes();
var countBytes = count.ToBigEndianBytes();
request[10] = countBytes[0];
request[11] = countBytes[1];
@@ -428,7 +432,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
// Function code
request[7] = (byte)ModbusFunctionCode.WriteSingleCoil;
byte[] addrBytes = coil.Address.ToBigEndianBytes();
var addrBytes = coil.Address.ToBigEndianBytes();
request[8] = addrBytes[0];
request[9] = addrBytes[1];
@@ -475,7 +479,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
// Function code
request[7] = (byte)ModbusFunctionCode.WriteSingleRegister;
byte[] addrBytes = register.Address.ToBigEndianBytes();
var addrBytes = register.Address.ToBigEndianBytes();
request[8] = addrBytes[0];
request[9] = addrBytes[1];
@@ -538,12 +542,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils;
// Starting address
byte[] addrBytes = firstAddress.ToBigEndianBytes();
var addrBytes = firstAddress.ToBigEndianBytes();
request[8] = addrBytes[0];
request[9] = addrBytes[1];
// Quantity
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
request[10] = countBytes[0];
request[11] = countBytes[1];
@@ -618,12 +622,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
// Starting address
byte[] addrBytes = firstAddress.ToBigEndianBytes();
var addrBytes = firstAddress.ToBigEndianBytes();
request[8] = addrBytes[0];
request[9] = addrBytes[1];
// Quantity
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
request[10] = countBytes[0];
request[11] = countBytes[1];
@@ -743,7 +747,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
// Transaction id
ushort txId = GetNextTransacitonId();
byte[] txBytes = txId.ToBigEndianBytes();
var txBytes = txId.ToBigEndianBytes();
header[0] = txBytes[0];
header[1] = txBytes[1];
@@ -752,7 +756,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
header[3] = 0x00;
// Number of following bytes
byte[] countBytes = ((ushort)followingBytes).ToBigEndianBytes();
var countBytes = ((ushort)followingBytes).ToBigEndianBytes();
header[4] = countBytes[0];
header[5] = countBytes[1];

View File

@@ -10,6 +10,22 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
/// </summary>
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
/// <summary>
@@ -96,12 +112,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[1] = (byte)ModbusFunctionCode.ReadCoils;
// Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes();
var addrBytes = startAddress.ToBigEndianBytes();
request[2] = addrBytes[0];
request[3] = addrBytes[1];
// Quantity
byte[] countBytes = count.ToBigEndianBytes();
var countBytes = count.ToBigEndianBytes();
request[4] = countBytes[0];
request[5] = countBytes[1];
@@ -156,12 +172,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[1] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
// Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes();
var addrBytes = startAddress.ToBigEndianBytes();
request[2] = addrBytes[0];
request[3] = addrBytes[1];
// Quantity
byte[] countBytes = count.ToBigEndianBytes();
var countBytes = count.ToBigEndianBytes();
request[4] = countBytes[0];
request[5] = countBytes[1];
@@ -216,12 +232,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[1] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
// Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes();
var addrBytes = startAddress.ToBigEndianBytes();
request[2] = addrBytes[0];
request[3] = addrBytes[1];
// Quantity
byte[] countBytes = count.ToBigEndianBytes();
var countBytes = count.ToBigEndianBytes();
request[4] = countBytes[0];
request[5] = countBytes[1];
@@ -273,12 +289,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[1] = (byte)ModbusFunctionCode.ReadInputRegisters;
// Starting address
byte[] addrBytes = startAddress.ToBigEndianBytes();
var addrBytes = startAddress.ToBigEndianBytes();
request[2] = addrBytes[0];
request[3] = addrBytes[1];
// Quantity
byte[] countBytes = count.ToBigEndianBytes();
var countBytes = count.ToBigEndianBytes();
request[4] = countBytes[0];
request[5] = countBytes[1];
@@ -394,7 +410,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
// Function code
request[1] = (byte)ModbusFunctionCode.WriteSingleCoil;
byte[] addrBytes = coil.Address.ToBigEndianBytes();
var addrBytes = coil.Address.ToBigEndianBytes();
request[2] = addrBytes[0];
request[3] = addrBytes[1];
@@ -438,7 +454,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
// Function code
request[1] = (byte)ModbusFunctionCode.WriteSingleRegister;
byte[] addrBytes = register.Address.ToBigEndianBytes();
var addrBytes = register.Address.ToBigEndianBytes();
request[2] = addrBytes[0];
request[3] = addrBytes[1];
@@ -495,11 +511,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[1] = (byte)ModbusFunctionCode.WriteMultipleCoils;
byte[] addrBytes = firstAddress.ToBigEndianBytes();
var addrBytes = firstAddress.ToBigEndianBytes();
request[2] = addrBytes[0];
request[3] = addrBytes[1];
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
request[4] = countBytes[0];
request[5] = countBytes[1];
@@ -565,11 +581,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[0] = unitId;
request[1] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
byte[] addrBytes = firstAddress.ToBigEndianBytes();
var addrBytes = firstAddress.ToBigEndianBytes();
request[2] = addrBytes[0];
request[3] = addrBytes[1];
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
request[4] = countBytes[0];
request[5] = countBytes[1];
@@ -627,7 +643,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
// - 0x03 Read Holding Registers
// - 0x04 Read Input Registers
// 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
if (responseBytes.Count < 5 + responseBytes[2])
@@ -638,7 +654,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
// - 0x06 Write Single Register
// - 0x0F Write Multiple Coils
// - 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 Multi => Unit ID, Function code, 2x Address, 2x QuantityWritten, 2x CRC
@@ -715,13 +731,13 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
if (isError)
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])
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)
throw new ModbusException("Number of bytes does not match.");
@@ -733,6 +749,10 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
/// <summary>
/// Calculate CRC16 for Modbus RTU.
/// </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="start">The start index.</param>
/// <param name="length">The number of bytes to calculate.</param>

View File

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

View File

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

View File

@@ -50,7 +50,8 @@ The different types handled by the Modbus Protocol.
In addition, you'll find the `DeviceIdentification` there.
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
@@ -59,8 +60,8 @@ Here you have the specific default implementations for the Modbus Protocol.
- ASCII
- RTU
- RTU over TCP
- TCP
- [RTU over TCP]
**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.
@@ -68,4 +69,10 @@ This validation is _not_ implemented here due to real world experience, that som
---
Published under MIT License (see [**tl;dr**Legal](https://www.tldrlegal.com/license/mit-license))
Published under MIT License (see [choose a license])
[RTU over TCP]: https://www.fernhillsoftware.com/help/drivers/modbus/modbus-protocol.html
[choose a license]: https://choosealicense.com/licenses/mit/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,30 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
#endregion Fields
#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
/// <inheritdoc cref="SerialPort.Handshake"/>
@@ -82,6 +106,10 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
set => _serialPort.Parity = value;
}
/// <inheritdoc cref="SerialPort.BytesToWrite"/>
public virtual int BytesToWrite
=> _serialPort.BytesToWrite;
/// <inheritdoc cref="SerialPort.BaudRate"/>
public virtual int BaudRate
{
@@ -89,6 +117,10 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
set => _serialPort.BaudRate = value;
}
/// <inheritdoc cref="SerialPort.BytesToRead"/>
public virtual int BytesToRead
=> _serialPort.BytesToRead;
#endregion Properties
#region Methods
@@ -101,6 +133,14 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
public virtual void 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"/>
public virtual void Dispose()
=> _serialPort.Dispose();
@@ -117,7 +157,7 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
/// <remarks>
/// There seems to be a bug with the async stream implementation on Windows.
/// <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>
/// <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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,62 +10,38 @@ using System.Threading.Tasks;
using AMWD.Protocols.Modbus.Common;
using AMWD.Protocols.Modbus.Common.Contracts;
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>
/// Implements a Modbus TCP server proxying all requests to a Modbus client of choice.
/// </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
private bool _isDisposed;
private TcpListener _listener;
private TimeSpan _readWriteTimeout = TimeSpan.FromSeconds(100);
private readonly TcpListenerWrapper _tcpListener = new(listenAddress, 502);
private CancellationTokenSource _stopCts;
private Task _clientConnectTask = Task.CompletedTask;
private readonly SemaphoreSlim _clientListLock = new(1, 1);
private readonly List<TcpClient> _clients = [];
private readonly List<Task> _clientTasks = [];
private readonly List<TcpClientWrapper> _clients = [];
#endregion Fields
#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
#region Properties
@@ -73,27 +49,46 @@ namespace AMWD.Protocols.Modbus.Proxy
/// <summary>
/// Gets the Modbus client used to request the remote device, that should be proxied.
/// </summary>
public ModbusClientBase Client { get; }
public ModbusClientBase Client { get; } = client ?? throw new ArgumentNullException(nameof(client));
/// <summary>
/// Gets the <see cref="IPAddress"/> to listen on.
/// </summary>
public IPAddress ListenAddress { get; }
public IPAddress ListenAddress
{
get => _tcpListener.LocalIPEndPoint.Address;
set => _tcpListener.LocalIPEndPoint.Address = value;
}
/// <summary>
/// Get the port to listen on.
/// </summary>
public int ListenPort { get; }
public int ListenPort
{
get => _tcpListener.LocalIPEndPoint.Port;
set => _tcpListener.LocalIPEndPoint.Port = value;
}
/// <summary>
/// Gets a value indicating whether the server is running.
/// </summary>
public bool IsRunning => _listener?.Server.IsBound ?? false;
public bool IsRunning => _tcpListener.Socket.IsBound;
/// <summary>
/// Gets or sets the read/write timeout for the incoming connections (not the <see cref="Client"/>!).
/// Default: 100 seconds.
/// </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
@@ -108,20 +103,17 @@ namespace AMWD.Protocols.Modbus.Proxy
Assertions();
_stopCts?.Cancel();
_listener?.Stop();
#if NET8_0_OR_GREATER
_listener?.Dispose();
#endif
_tcpListener.Stop();
_stopCts?.Dispose();
_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)
_listener.Server.DualMode = true;
_tcpListener.Socket.DualMode = true;
_listener.Start();
_tcpListener.Start();
_clientConnectTask = WaitForClientAsync(_stopCts.Token);
return Task.CompletedTask;
@@ -139,24 +131,12 @@ namespace AMWD.Protocols.Modbus.Proxy
private async Task StopAsyncInternal(CancellationToken cancellationToken = default)
{
_stopCts.Cancel();
_listener.Stop();
#if NET8_0_OR_GREATER
_listener.Dispose();
#endif
try
{
await Task.WhenAny(_clientConnectTask, Task.Delay(Timeout.Infinite, cancellationToken));
}
catch (OperationCanceledException)
{
// Terminated
}
_stopCts?.Cancel();
_tcpListener.Stop();
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)
{
@@ -178,8 +158,10 @@ namespace AMWD.Protocols.Modbus.Proxy
_clientListLock.Dispose();
_clients.Clear();
_tcpListener.Dispose();
_stopCts?.Dispose();
GC.SuppressFinalize(this);
}
private void Assertions()
@@ -202,16 +184,13 @@ namespace AMWD.Protocols.Modbus.Proxy
{
try
{
#if NET8_0_OR_GREATER
var client = await _listener.AcceptTcpClientAsync(cancellationToken);
#else
var client = await _listener.AcceptTcpClientAsync();
#endif
await _clientListLock.WaitAsync(cancellationToken);
var client = await _tcpListener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
try
{
_clients.Add(client);
_clientTasks.Add(HandleClientAsync(client, cancellationToken));
// Can be ignored as it will terminate by itself on cancellation
HandleClientAsync(client, cancellationToken).Forget();
}
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
{
@@ -234,23 +213,32 @@ namespace AMWD.Protocols.Modbus.Proxy
{
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 (cancellationToken.Register(cts.Cancel))
{
byte[] headerBytes = await stream.ReadExpectedBytesAsync(6, cts.Token);
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);
byte[] bodyBytes = await stream.ReadExpectedBytesAsync(length, cts.Token).ConfigureAwait(continueOnCapturedContext: false);
requestBytes.AddRange(bodyBytes);
}
byte[] responseBytes = await HandleRequestAsync([.. requestBytes], cancellationToken);
byte[] responseBytes = await HandleRequestAsync([.. requestBytes], cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
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
@@ -259,7 +247,7 @@ namespace AMWD.Protocols.Modbus.Proxy
}
finally
{
await _clientListLock.WaitAsync(cancellationToken);
await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
try
{
_clients.Remove(client);
@@ -309,14 +297,14 @@ namespace AMWD.Protocols.Modbus.Proxy
default: // unknown function
{
byte[] responseBytes = new byte[9];
Array.Copy(requestBytes, 0, responseBytes, 0, 8);
var responseBytes = new List<byte>();
responseBytes.AddRange(requestBytes.Take(8));
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
// Mark as error
responseBytes[7] |= 0x80;
responseBytes[8] = (byte)ModbusErrorCode.IllegalFunction;
return Task.FromResult(responseBytes);
return Task.FromResult(ReturnResponse(responseBytes));
}
}
}
@@ -334,7 +322,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.AddRange(requestBytes.Take(8));
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)];
for (int i = 0; i < coils.Count; i++)
@@ -357,7 +345,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
return [.. responseBytes];
return ReturnResponse(responseBytes);
}
private async Task<byte[]> HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -373,7 +361,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.AddRange(requestBytes.Take(8));
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)];
for (int i = 0; i < discreteInputs.Count; i++)
@@ -396,7 +384,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
return [.. responseBytes];
return ReturnResponse(responseBytes);
}
private async Task<byte[]> HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -412,7 +400,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.AddRange(requestBytes.Take(8));
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];
for (int i = 0; i < holdingRegisters.Count; i++)
@@ -430,7 +418,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
return [.. responseBytes];
return ReturnResponse(responseBytes);
}
private async Task<byte[]> HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -446,7 +434,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.AddRange(requestBytes.Take(8));
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];
for (int i = 0; i < count; i++)
@@ -464,7 +452,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
return [.. responseBytes];
return ReturnResponse(responseBytes);
}
private async Task<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -481,7 +469,8 @@ namespace AMWD.Protocols.Modbus.Proxy
{
responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
return [.. responseBytes];
return ReturnResponse(responseBytes);
}
try
@@ -493,7 +482,7 @@ namespace AMWD.Protocols.Modbus.Proxy
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)
{
// Response is an echo of the request
@@ -511,7 +500,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
return [.. responseBytes];
return ReturnResponse(responseBytes);
}
private async Task<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -533,7 +522,7 @@ namespace AMWD.Protocols.Modbus.Proxy
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)
{
// Response is an echo of the request
@@ -551,7 +540,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
return [.. responseBytes];
return ReturnResponse(responseBytes);
}
private async Task<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -570,7 +559,8 @@ namespace AMWD.Protocols.Modbus.Proxy
{
responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
return [.. responseBytes];
return ReturnResponse(responseBytes);
}
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)
{
// Response is an echo of the request
@@ -610,7 +600,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
return [.. responseBytes];
return ReturnResponse(responseBytes);
}
private async Task<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -629,7 +619,8 @@ namespace AMWD.Protocols.Modbus.Proxy
{
responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
return [.. responseBytes];
return ReturnResponse(responseBytes);
}
try
@@ -646,8 +637,9 @@ namespace AMWD.Protocols.Modbus.Proxy
HighByte = requestBytes[baseOffset + i * 2],
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)
{
// Response is an echo of the request
@@ -659,18 +651,20 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
}
}
catch
{
responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
return [.. responseBytes];
return ReturnResponse(responseBytes);
}
private async Task<byte[]> HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken)
{
if (requestBytes.Length < 11)
return null;
var responseBytes = new List<byte>();
responseBytes.AddRange(requestBytes.Take(8));
@@ -678,7 +672,8 @@ namespace AMWD.Protocols.Modbus.Proxy
{
responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
return [.. responseBytes];
return ReturnResponse(responseBytes);
}
var firstObject = (ModbusDeviceIdentificationObject)requestBytes[10];
@@ -686,7 +681,8 @@ namespace AMWD.Protocols.Modbus.Proxy
{
responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
return [.. responseBytes];
return ReturnResponse(responseBytes);
}
var category = (ModbusDeviceIdentificationCategory)requestBytes[9];
@@ -694,12 +690,13 @@ namespace AMWD.Protocols.Modbus.Proxy
{
responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
return [.. responseBytes];
return ReturnResponse(responseBytes);
}
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>();
@@ -708,31 +705,20 @@ namespace AMWD.Protocols.Modbus.Proxy
// Conformity
bodyBytes.Add((byte)category);
if (res.IsIndividualAccessAllowed)
if (deviceInfo.IsIndividualAccessAllowed)
bodyBytes[2] |= 0x80;
// More, NextId, NumberOfObjects
bodyBytes.AddRange(new byte[3]);
int maxObjectId;
switch (category)
int maxObjectId = category switch
{
case ModbusDeviceIdentificationCategory.Basic:
maxObjectId = 0x02;
break;
case ModbusDeviceIdentificationCategory.Regular:
maxObjectId = 0x06;
break;
case ModbusDeviceIdentificationCategory.Extended:
maxObjectId = 0xFF;
break;
default: // Individual
maxObjectId = requestBytes[10];
break;
}
ModbusDeviceIdentificationCategory.Basic => 0x02,
ModbusDeviceIdentificationCategory.Regular => 0x06,
ModbusDeviceIdentificationCategory.Extended => 0xFF,
// Individual
_ => requestBytes[10],
};
byte numberOfObjects = 0;
for (int i = requestBytes[10]; i <= maxObjectId; i++)
@@ -741,7 +727,7 @@ namespace AMWD.Protocols.Modbus.Proxy
if (0x07 <= i && i <= 0x7F)
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
if (responseBytes.Count + bodyBytes.Count + objBytes.Length > TcpProtocol.MAX_ADU_LENGTH)
@@ -751,7 +737,8 @@ namespace AMWD.Protocols.Modbus.Proxy
bodyBytes[5] = numberOfObjects;
responseBytes.AddRange(bodyBytes);
return [.. responseBytes];
return ReturnResponse(responseBytes);
}
bodyBytes.AddRange(objBytes);
@@ -760,24 +747,26 @@ namespace AMWD.Protocols.Modbus.Proxy
bodyBytes[5] = numberOfObjects;
responseBytes.AddRange(bodyBytes);
return [.. responseBytes];
return ReturnResponse(responseBytes);
}
catch
{
responseBytes[7] |= 0x80;
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 };
switch ((ModbusDeviceIdentificationObject)objectId)
{
case ModbusDeviceIdentificationObject.VendorName:
{
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName);
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName ?? "");
result.Add((byte)bytes.Length);
result.AddRange(bytes);
}
@@ -785,7 +774,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.ProductCode:
{
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode);
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode ?? "");
result.Add((byte)bytes.Length);
result.AddRange(bytes);
}
@@ -793,7 +782,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.MajorMinorRevision:
{
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision);
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision ?? "");
result.Add((byte)bytes.Length);
result.AddRange(bytes);
}
@@ -801,7 +790,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.VendorUrl:
{
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl);
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl ?? "");
result.Add((byte)bytes.Length);
result.AddRange(bytes);
}
@@ -809,7 +798,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.ProductName:
{
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName);
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName ?? "");
result.Add((byte)bytes.Length);
result.AddRange(bytes);
}
@@ -817,7 +806,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.ModelName:
{
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName);
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName ?? "");
result.Add((byte)bytes.Length);
result.AddRange(bytes);
}
@@ -825,7 +814,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.UserApplicationName:
{
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName);
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName ?? "");
result.Add((byte)bytes.Length);
result.AddRange(bytes);
}
@@ -833,9 +822,8 @@ namespace AMWD.Protocols.Modbus.Proxy
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.AddRange(bytes);
}
@@ -850,6 +838,29 @@ namespace AMWD.Protocols.Modbus.Proxy
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
/// <inheritdoc/>
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine($"TCP Proxy");
sb.AppendLine($" {nameof(ListenAddress)}: {ListenAddress}");
sb.AppendLine($" {nameof(ListenPort)}: {ListenPort}");
sb.AppendLine($" {nameof(Client)}: {Client.GetType().Name}");
return sb.ToString();
}
}
}

View File

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

View File

@@ -23,14 +23,14 @@ float voltage = registers.GetSingle();
Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V");
```
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
// [...]
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.0b]: https://modbus.org/docs/Modbus_Messaging_Implementation_Guide_V1_0b.pdf
[**tl;dr**Legal]: https://www.tldrlegal.com/license/mit-license
[choose a license]: https://choosealicense.com/licenses/mit/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,50 @@
<Project>
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/AM-WD/AMWD.Protocols.Modbus.git</RepositoryUrl>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EmbedUntrackedSources>false</EmbedUntrackedSources>
<PackageIcon>package-icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
<PackageProjectUrl>https://modbus.org/tech.php</PackageProjectUrl>
<AssemblyOriginatorKeyFile>../../AMWD.Protocols.Modbus.snk</AssemblyOriginatorKeyFile>
</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>
<None Include="../../package-icon.png" Pack="true" PackagePath="/" />
<None Include="../../LICENSE.txt" Pack="true" PackagePath="/" />
<None Include="README.md" Pack="true" PackagePath="/" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="AMWD.Protocols.Modbus.Tests" PublicKey="$(PublicKey)" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" PublicKey="$(MoqPublicKey)" />
</ItemGroup>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)\..'))" />
</Project>

View File

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

View File

@@ -1,8 +1,7 @@
using System.Collections.Generic;
using System.Text;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using AMWD.Protocols.Modbus.Common.Contracts;
namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
{
@@ -105,19 +104,16 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowExceptionOnNullConnection()
{
// Arrange
IModbusConnection connection = null;
// Act
new ModbusClientBaseWrapper(connection);
// Assert - ArgumentNullException
// Act + Assert
Assert.ThrowsExactly<ArgumentNullException>(() => new ModbusClientBaseWrapper(connection));
}
[DataTestMethod]
[TestMethod]
[DataRow(true)]
[DataRow(false)]
public void ShouldAlsoDisposeConnection(bool disposeConnection)
@@ -155,31 +151,25 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
}
[TestMethod]
[ExpectedException(typeof(ObjectDisposedException))]
public async Task ShouldAssertDisposed()
{
// Arrange
var client = GetClient();
client.Dispose();
// Act
await client.ReadCoilsAsync(UNIT_ID, START_ADDRESS, READ_COUNT);
// Assert - ObjectDisposedException
// Act + Assert
await Assert.ThrowsExactlyAsync<ObjectDisposedException>(() => client.ReadCoilsAsync(UNIT_ID, START_ADDRESS, READ_COUNT));
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public async Task ShouldAssertProtocolSet()
{
// Arrange
var client = GetClient();
client.Protocol = null;
// Act
await client.ReadCoilsAsync(UNIT_ID, START_ADDRESS, READ_COUNT);
// Assert - ArgumentNullException
// Act + Assert
await Assert.ThrowsExactlyAsync<ArgumentNullException>(() => client.ReadCoilsAsync(UNIT_ID, START_ADDRESS, READ_COUNT));
}
#endregion Common/Connection/Assertions

View File

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

View File

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

View File

@@ -32,31 +32,23 @@
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowNullForGetSByte()
{
// Arrange
HoldingRegister register = null;
// Act
register.GetSByte();
// Assert - ArgumentNullException
Assert.Fail();
// Act + Assert
Assert.ThrowsExactly<ArgumentNullException>(() => register.GetSByte());
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentForGetSByte()
{
// Arrange
var obj = new Coil();
// Act
obj.GetSByte();
// Assert - ArgumentException
Assert.Fail();
// Act + Assert
Assert.ThrowsExactly<ArgumentException>(() => obj.GetSByte());
}
[TestMethod]
@@ -86,31 +78,23 @@
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowNullForGetInt16()
{
// Arrange
HoldingRegister register = null;
// Act
register.GetInt16();
// Assert - ArgumentNullException
Assert.Fail();
// Act + Assert
Assert.ThrowsExactly<ArgumentNullException>(() => register.GetInt16());
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentForGetInt16()
{
// Arrange
var obj = new Coil();
// Act
obj.GetInt16();
// Assert - ArgumentException
Assert.Fail();
// Act + Assert
Assert.ThrowsExactly<ArgumentException>(() => obj.GetInt16());
}
[TestMethod]
@@ -149,21 +133,16 @@
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowNullOnGetInt32()
{
// Arrange
HoldingRegister[] registers = null;
// Act
registers.GetInt32(0);
// Assert - ArgumentNullException
Assert.Fail();
// Act + Assert
Assert.ThrowsExactly<ArgumentNullException>(() => registers.GetInt32(0));
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentOnGetInt32ForLength()
{
// Arrange
@@ -172,17 +151,13 @@
new HoldingRegister { Address = 101, HighByte = 0x01, LowByte = 0x02 }
};
// Act
registers.GetInt32(0);
// Assert - ArgumentException
Assert.Fail();
// Act + Assert
Assert.ThrowsExactly<ArgumentException>(() => registers.GetInt32(0));
}
[DataTestMethod]
[TestMethod]
[DataRow(1)]
[DataRow(-1)]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ShouldThrowArgumentOutOfRangeOnGetInt32(int startIndex)
{
// Arrange
@@ -192,15 +167,11 @@
new HoldingRegister { Address = 100, HighByte = 0x03, LowByte = 0x04 }
};
// Act
registers.GetInt32(startIndex);
// Assert - ArgumentOutOfRangeException
Assert.Fail();
// Act + Assert
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => registers.GetInt32(startIndex));
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentOnGetInt32ForType()
{
// Arrange
@@ -210,11 +181,8 @@
new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 }
};
// Act
registers.GetInt32(0);
// Assert - ArgumentException
Assert.Fail();
// Act + Assert
Assert.ThrowsExactly<ArgumentException>(() => registers.GetInt32(0));
}
[TestMethod]
@@ -257,21 +225,16 @@
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowNullOnGetInt64()
{
// Arrange
HoldingRegister[] registers = null;
// Act
registers.GetInt64(0);
// Assert - ArgumentNullException
Assert.Fail();
// Act + Assert
Assert.ThrowsExactly<ArgumentNullException>(() => registers.GetInt64(0));
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentOnGetInt64ForLength()
{
// Arrange
@@ -282,17 +245,13 @@
new HoldingRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
};
// Act
registers.GetInt64(0);
// Assert - ArgumentException
Assert.Fail();
// Act + Assert
Assert.ThrowsExactly<ArgumentException>(() => registers.GetInt64(0));
}
[DataTestMethod]
[TestMethod]
[DataRow(1)]
[DataRow(-1)]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ShouldThrowArgumentOutOfRangeOnGetInt64(int startIndex)
{
// Arrange
@@ -304,15 +263,11 @@
new HoldingRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
};
// Act
registers.GetInt64(startIndex);
// Assert - ArgumentOutOfRangeException
Assert.Fail();
// Act + Assert
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => registers.GetInt64(startIndex));
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ShouldThrowArgumentOnGetInt64ForType()
{
// Arrange
@@ -324,11 +279,8 @@
new InputRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
};
// Act
registers.GetInt64(0);
// Assert - ArgumentException
Assert.Fail();
// Act + Assert
Assert.ThrowsExactly<ArgumentException>(() => registers.GetInt64(0));
}
#endregion Modbus to value

View File

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

View File

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

View File

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

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