Implementation of the basic functionallity
This commit is contained in:
159
.editorconfig
Normal file
159
.editorconfig
Normal file
@@ -0,0 +1,159 @@
|
||||
# Documentation:
|
||||
# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference
|
||||
|
||||
# Top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
insert_final_newline = true
|
||||
end_of_line = crlf
|
||||
indent_style = tab
|
||||
|
||||
[*.{cs,vb}]
|
||||
# Sort using and Import directives with System.* appearing first
|
||||
dotnet_sort_system_directives_first = true
|
||||
|
||||
# Avoid "this." and "Me." if not necessary
|
||||
dotnet_style_qualification_for_event = false:warning
|
||||
dotnet_style_qualification_for_field = false:warning
|
||||
dotnet_style_qualification_for_method = false:warning
|
||||
dotnet_style_qualification_for_property = false:warning
|
||||
|
||||
# Use language keywords instead of framework type names for type references
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:warning
|
||||
dotnet_style_predefined_type_for_member_access = true:warning
|
||||
|
||||
# Suggest explicit accessibility modifiers
|
||||
dotnet_style_require_accessibility_modifiers = always:suggestion
|
||||
|
||||
# Suggest more modern language features when available
|
||||
dotnet_style_explicit_tuple_names = true:warning
|
||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = true:none
|
||||
dotnet_style_prefer_conditional_expression_over_return = true:none
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
|
||||
# Definitions
|
||||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum, delegate, type_parameter
|
||||
dotnet_naming_symbols.methods_properties.applicable_kinds = method, local_function, property
|
||||
dotnet_naming_symbols.public_symbols.applicable_kinds = property, method, field, event
|
||||
dotnet_naming_symbols.public_symbols.applicable_accessibilities = public
|
||||
dotnet_naming_symbols.constant_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.constant_fields.required_modifiers = const
|
||||
dotnet_naming_symbols.private_protected_internal_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_protected_internal_fields.applicable_accessibilities = private, protected, internal
|
||||
dotnet_naming_symbols.parameters_locals.applicable_kinds = parameter, local
|
||||
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
|
||||
dotnet_naming_style.camel_case_style.capitalization = camel_case
|
||||
|
||||
# Name all types using PascalCase
|
||||
dotnet_naming_rule.types_must_be_capitalized.symbols = types
|
||||
dotnet_naming_rule.types_must_be_capitalized.style = pascal_case_style
|
||||
dotnet_naming_rule.types_must_be_capitalized.severity = warning
|
||||
|
||||
# Name all methods and properties using PascalCase
|
||||
dotnet_naming_rule.methods_properties_must_be_capitalized.symbols = methods_properties
|
||||
dotnet_naming_rule.methods_properties_must_be_capitalized.style = pascal_case_style
|
||||
dotnet_naming_rule.methods_properties_must_be_capitalized.severity = warning
|
||||
|
||||
# Name all public members using PascalCase
|
||||
dotnet_naming_rule.public_members_must_be_capitalized.symbols = public_symbols
|
||||
dotnet_naming_rule.public_members_must_be_capitalized.style = pascal_case_style
|
||||
dotnet_naming_rule.public_members_must_be_capitalized.severity = warning
|
||||
|
||||
# Name all constant fields using PascalCase
|
||||
dotnet_naming_rule.constant_fields_must_be_pascal_case.symbols = constant_fields
|
||||
dotnet_naming_rule.constant_fields_must_be_pascal_case.style = pascal_case_style
|
||||
dotnet_naming_rule.constant_fields_must_be_pascal_case.severity = suggestion
|
||||
|
||||
# Name all private and internal fields using camelCase
|
||||
dotnet_naming_rule.private_protected_internal_fields_must_be_camel_case.symbols = private_protected_internal_fields
|
||||
dotnet_naming_rule.private_protected_internal_fields_must_be_camel_case.style = camel_case_style
|
||||
dotnet_naming_rule.private_protected_internal_fields_must_be_camel_case.severity = warning
|
||||
|
||||
# Name all parameters and locals using camelCase
|
||||
dotnet_naming_rule.parameters_locals_must_be_camel_case.symbols = parameters_locals
|
||||
dotnet_naming_rule.parameters_locals_must_be_camel_case.style = camel_case_style
|
||||
dotnet_naming_rule.parameters_locals_must_be_camel_case.severity = warning
|
||||
|
||||
# Name all private fields starting with underscore
|
||||
dotnet_naming_rule.private_members_with_underscore.symbols = private_fields
|
||||
dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
|
||||
dotnet_naming_rule.private_members_with_underscore.severity = suggestion
|
||||
|
||||
dotnet_naming_symbols.private_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
|
||||
|
||||
dotnet_naming_style.prefix_underscore.capitalization = camel_case
|
||||
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
|
||||
|
||||
# Only use "var" when it's obvious what the variable type is
|
||||
csharp_style_var_for_built_in_types = false:warning
|
||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
csharp_style_var_elsewhere = false:none
|
||||
|
||||
# Prefer method-like constructs to have a block body
|
||||
csharp_style_expression_bodied_methods = false:none
|
||||
csharp_style_expression_bodied_constructors = false:none
|
||||
csharp_style_expression_bodied_operators = false:none
|
||||
|
||||
# Prefer property-like constructs to have an expression-body
|
||||
csharp_style_expression_bodied_properties = true:none
|
||||
csharp_style_expression_bodied_indexers = true:none
|
||||
csharp_style_expression_bodied_accessors = true:none
|
||||
|
||||
# Suggest more modern language features when available
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
csharp_style_pattern_local_over_anonymous_function = true:suggestion
|
||||
|
||||
# Newline settings
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
|
||||
# Indentation preferences
|
||||
csharp_indent_block_contents = true
|
||||
csharp_indent_braces = false
|
||||
csharp_indent_case_contents = true
|
||||
csharp_indent_switch_labels = true
|
||||
csharp_indent_labels = one_less_than_current
|
||||
|
||||
# Space preferences
|
||||
csharp_space_after_cast = false
|
||||
csharp_space_after_colon_in_inheritance_clause = true
|
||||
csharp_space_after_comma = true
|
||||
csharp_space_after_dot = false
|
||||
csharp_space_after_keywords_in_control_flow_statements = true
|
||||
csharp_space_after_semicolon_in_for_statement = true
|
||||
csharp_space_around_binary_operators = before_and_after
|
||||
csharp_space_around_declaration_statements = do_not_ignore
|
||||
csharp_space_before_colon_in_inheritance_clause = true
|
||||
csharp_space_before_comma = false
|
||||
csharp_space_before_dot = false
|
||||
csharp_space_before_open_square_brackets = false
|
||||
csharp_space_before_semicolon_in_for_statement = false
|
||||
csharp_space_between_empty_square_brackets = false
|
||||
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
||||
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_name_and_open_parenthesis = false
|
||||
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||
csharp_space_between_parentheses = false
|
||||
csharp_space_between_square_brackets = false
|
||||
|
||||
[*.{xml,csproj,targets,props,json,yml}]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
107
.gitlab-ci.yml
Normal file
107
.gitlab-ci.yml
Normal file
@@ -0,0 +1,107 @@
|
||||
# The image has to use the same version as the .NET UnitTest project
|
||||
image: mcr.microsoft.com/dotnet/sdk:8.0
|
||||
|
||||
variables:
|
||||
TZ: "Europe/Berlin"
|
||||
LANG: "de"
|
||||
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
- deploy
|
||||
|
||||
|
||||
|
||||
build-debug:
|
||||
stage: build
|
||||
tags:
|
||||
- docker
|
||||
- lnx
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG == null
|
||||
script:
|
||||
- dotnet restore --no-cache --force
|
||||
- dotnet build -c Debug --nologo --no-restore --no-incremental
|
||||
- mkdir ./artifacts
|
||||
- mv ./AMWD.Protocols.Modbus.Common/bin/Debug/*.nupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Common/bin/Debug/*.snupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Serial/bin/Debug/*.nupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Serial/bin/Debug/*.snupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Debug/*.nupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Debug/*.snupkg ./artifacts/
|
||||
artifacts:
|
||||
paths:
|
||||
- artifacts/*.nupkg
|
||||
- artifacts/*.snupkg
|
||||
expire_in: 3 days
|
||||
|
||||
test-debug:
|
||||
stage: test
|
||||
dependencies:
|
||||
- build-debug
|
||||
tags:
|
||||
- docker
|
||||
- lnx
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG == null
|
||||
# line-coverage
|
||||
#coverage: '/Total[^|]*\|\s*([0-9.%]+)/'
|
||||
# branch-coverage
|
||||
coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/'
|
||||
script:
|
||||
- dotnet restore --no-cache --force
|
||||
- dotnet test -c Debug --nologo --no-restore
|
||||
|
||||
|
||||
build-release:
|
||||
stage: build
|
||||
tags:
|
||||
- docker
|
||||
- lnx
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG != null
|
||||
script:
|
||||
- dotnet restore --no-cache --force
|
||||
- dotnet build -c Release --nologo --no-restore --no-incremental
|
||||
- mkdir ./artifacts
|
||||
- mv ./AMWD.Protocols.Modbus.Common/bin/Release/*.nupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Common/bin/Release/*.snupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Serial/bin/Release/*.nupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Serial/bin/Release/*.snupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Release/*.nupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Release/*.snupkg ./artifacts/
|
||||
artifacts:
|
||||
paths:
|
||||
- artifacts/*.nupkg
|
||||
- artifacts/*.snupkg
|
||||
expire_in: 1 day
|
||||
|
||||
test-release:
|
||||
stage: test
|
||||
dependencies:
|
||||
- build-release
|
||||
tags:
|
||||
- docker
|
||||
- lnx
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG != null
|
||||
# line-coverage
|
||||
#coverage: '/Total[^|]*\|\s*([0-9.%]+)/'
|
||||
# branch-coverage
|
||||
coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/'
|
||||
script:
|
||||
- dotnet restore --no-cache --force
|
||||
- dotnet test -c Release --nologo --no-restore
|
||||
|
||||
deploy:
|
||||
stage: deploy
|
||||
dependencies:
|
||||
- build-release
|
||||
- test-release
|
||||
tags:
|
||||
- docker
|
||||
- lnx
|
||||
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
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
|
||||
<AssemblyName>amwd-modbus-common</AssemblyName>
|
||||
<RootNamespace>AMWD.Protocols.Modbus.Common</RootNamespace>
|
||||
|
||||
<Product>Modbus Protocol Common</Product>
|
||||
<Description>Common data for Modbus protocol.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
46
AMWD.Protocols.Modbus.Common/Contracts/IModbusConnection.cs
Normal file
46
AMWD.Protocols.Modbus.Common/Contracts/IModbusConnection.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a Modbus connection.
|
||||
/// </summary>
|
||||
public interface IModbusConnection : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The connection type name.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the connection is open.
|
||||
/// </summary>
|
||||
bool IsConnected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Opens the connection to the remote device.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns>An awaitable <see cref="Task"/>.</returns>
|
||||
Task ConnectAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Closes the connection to the remote device.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns>An awaitable <see cref="Task"/>.</returns>
|
||||
Task DisconnectAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes a Modbus request.
|
||||
/// </summary>
|
||||
/// <param name="request">The Modbus request serialized in bytes.</param>
|
||||
/// <param name="validateResponseComplete">A function to validate whether the response is complete.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns>A list of <see cref="byte"/>s containing the response.</returns>
|
||||
Task<IReadOnlyList<byte>> InvokeAsync(IReadOnlyList<byte> request, Func<IReadOnlyList<byte>, bool> validateResponseComplete, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
165
AMWD.Protocols.Modbus.Common/Contracts/IModbusProtocol.cs
Normal file
165
AMWD.Protocols.Modbus.Common/Contracts/IModbusProtocol.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// A definition of the capabilities an implementation of the Modbus protocol version should have.
|
||||
/// </summary>
|
||||
public interface IModbusProtocol
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the protocol type name.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
#region Read
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a read request for <see cref="Coil"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="startAddress">The starting address.</param>
|
||||
/// <param name="count">The number of coils to read.</param>
|
||||
/// <returns>The <see langword="byte"/>s to send.</returns>
|
||||
IReadOnlyList<byte> SerializeReadCoils(byte unitId, ushort startAddress, ushort count);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a read response for <see cref="Coil"/>s.
|
||||
/// </summary>
|
||||
/// <param name="response">The <see langword="byte"/>s received.</param>
|
||||
/// <returns>A list of <see cref="Coil"/>s.</returns>
|
||||
IReadOnlyList<Coil> DeserializeReadCoils(IReadOnlyList<byte> response);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a read request for <see cref="DiscreteInput"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="startAddress">The starting address.</param>
|
||||
/// <param name="count">The number of discrete inputs to read.</param>
|
||||
/// <returns>The <see langword="byte"/>s to send.</returns>
|
||||
IReadOnlyList<byte> SerializeReadDiscreteInputs(byte unitId, ushort startAddress, ushort count);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a read response for <see cref="DiscreteInput"/>s.
|
||||
/// </summary>
|
||||
/// <param name="response">The <see langword="byte"/>s received.</param>
|
||||
/// <returns>A list of <see cref="DiscreteInput"/>s.</returns>
|
||||
IReadOnlyList<DiscreteInput> DeserializeReadDiscreteInputs(IReadOnlyList<byte> response);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a read request for <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="startAddress">The starting address.</param>
|
||||
/// <param name="count">The number of holding registers to read.</param>
|
||||
/// <returns>The <see langword="byte"/>s to send.</returns>
|
||||
IReadOnlyList<byte> SerializeReadHoldingRegisters(byte unitId, ushort startAddress, ushort count);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a read response for <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="response">The <see langword="byte"/>s received.</param>
|
||||
/// <returns>A list of <see cref="HoldingRegister"/>s.</returns>
|
||||
IReadOnlyList<HoldingRegister> DeserializeReadHoldingRegisters(IReadOnlyList<byte> response);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a read request for <see cref="InputRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="startAddress">The starting address.</param>
|
||||
/// <param name="count">The number of input registers to read.</param>
|
||||
/// <returns>The <see langword="byte"/>s to send.</returns>
|
||||
IReadOnlyList<byte> SerializeReadInputRegisters(byte unitId, ushort startAddress, ushort count);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a read response for <see cref="InputRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="response">The <see langword="byte"/>s received.</param>
|
||||
/// <returns>A list of <see cref="InputRegister"/>s.</returns>
|
||||
IReadOnlyList<InputRegister> DeserializeReadInputRegisters(IReadOnlyList<byte> response);
|
||||
|
||||
#endregion Read
|
||||
|
||||
#region Write
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a write request for a single <see cref="Coil"/>.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="coil">The coil to write.</param>
|
||||
/// <returns>The <see langword="byte"/>s to send.</returns>
|
||||
IReadOnlyList<byte> SerializeWriteSingleCoil(byte unitId, Coil coil);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a write response for a single <see cref="Coil"/>.
|
||||
/// </summary>
|
||||
/// <param name="response">The <see langword="byte"/>s received.</param>
|
||||
/// <returns>Should be the coil itself, as the response is an echo of the request.</returns>
|
||||
Coil DeserializeWriteSingleCoil(IReadOnlyList<byte> response);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a write request for a single <see cref="HoldingRegister"/>.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="register">The holding register to write.</param>
|
||||
/// <returns>The <see langword="byte"/>s to send.</returns>
|
||||
IReadOnlyList<byte> SerializeWriteSingleHoldingRegister(byte unitId, HoldingRegister register);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a write response for a single <see cref="HoldingRegister"/>.
|
||||
/// </summary>
|
||||
/// <param name="response">The <see langword="byte"/>s received.</param>
|
||||
/// <returns>Should be the holding register itself, as the response is an echo of the request.</returns>
|
||||
HoldingRegister DeserializeWriteSingleHoldingRegister(IReadOnlyList<byte> response);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a write request for multiple <see cref="Coil"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="coils">The coils to write.</param>
|
||||
/// <returns>The <see langword="byte"/>s to send.</returns>
|
||||
IReadOnlyList<byte> SerializeWriteMultipleCoils(byte unitId, IReadOnlyList<Coil> coils);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a write response for multiple <see cref="Coil"/>s.
|
||||
/// </summary>
|
||||
/// <param name="response">The <see langword="byte"/>s received.</param>
|
||||
/// <returns>A tuple containting the first address and the number of coils written.</returns>
|
||||
(ushort FirstAddress, ushort NumberOfCoils) DeserializeWriteMultipleCoils(IReadOnlyList<byte> response);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a write request for multiple <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="registers">The holding registers to write.</param>
|
||||
/// <returns>The <see langword="byte"/>s to send.</returns>
|
||||
IReadOnlyList<byte> SerializeWriteMultipleHoldingRegisters(byte unitId, IReadOnlyList<HoldingRegister> registers);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a write response for multiple <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="response">The <see langword="byte"/>s received.</param>
|
||||
/// <returns>A tuple containting the first address and the number of holding registers written.</returns>
|
||||
(ushort FirstAddress, ushort NumberOfRegisters) DeserializeWriteMultipleHoldingRegisters(IReadOnlyList<byte> response);
|
||||
|
||||
#endregion Write
|
||||
|
||||
#region Control
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the receive response bytes are complete to deserialize the response.
|
||||
/// </summary>
|
||||
/// <param name="responseBytes">The already received response bytes.</param>
|
||||
/// <returns><see langword="true"/> when the response is complete, otherwise <see langword="false"/>.</returns>
|
||||
bool CheckResponseComplete(IReadOnlyList<byte> responseBytes);
|
||||
|
||||
/// <summary>
|
||||
/// Validates the response against the request and throws <see cref="ModbusException"/>s if necessary.
|
||||
/// </summary>
|
||||
/// <param name="request">The serialized request.</param>
|
||||
/// <param name="response">The received response.</param>
|
||||
void ValidateResponse(IReadOnlyList<byte> request, IReadOnlyList<byte> response);
|
||||
|
||||
#endregion Control
|
||||
}
|
||||
}
|
||||
315
AMWD.Protocols.Modbus.Common/Contracts/ModbusClientBase.cs
Normal file
315
AMWD.Protocols.Modbus.Common/Contracts/ModbusClientBase.cs
Normal file
@@ -0,0 +1,315 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Linq;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Base implementation of a Modbus client.
|
||||
/// </summary>
|
||||
public abstract class ModbusClientBase : 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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="IModbusConnection"/> responsible for invoking the requests.
|
||||
/// </summary>
|
||||
protected readonly IModbusConnection connection;
|
||||
|
||||
/// <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>
|
||||
public ModbusClientBase(IModbusConnection connection)
|
||||
: 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 a value indicating whether the client is connected.
|
||||
/// </summary>
|
||||
public bool IsConnected => connection.IsConnected;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the protocol type to use.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The default protocol used by the client should be initialized in the constructor.
|
||||
/// </remarks>
|
||||
public abstract IModbusProtocol Protocol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Starts the connection to the remote endpoint.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns>An awaitable <see cref="Task"/>.</returns>
|
||||
public virtual Task ConnectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Assertions(false);
|
||||
return connection.ConnectAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the connection to the remote endpoint.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns>An awaitable <see cref="Task"/>.</returns>
|
||||
public virtual Task DisconnectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Assertions(false);
|
||||
return connection.DisconnectAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads multiple <see cref="Coil"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="startAddress">The starting address.</param>
|
||||
/// <param name="count">The number of coils to read.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns>A list of <see cref="Coil"/>s.</returns>
|
||||
public virtual async Task<IReadOnlyList<Coil>> ReadCoilsAsync(byte unitId, ushort startAddress, ushort count, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeReadCoils(unitId, startAddress, count);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
// The protocol processes complete bytes from the response.
|
||||
// So reduce to the actual coil count.
|
||||
var coils = Protocol.DeserializeReadCoils(response).Take(count);
|
||||
foreach (var coil in coils)
|
||||
coil.Address += startAddress;
|
||||
|
||||
return coils.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads multiple <see cref="DiscreteInput"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="startAddress">The starting address.</param>
|
||||
/// <param name="count">The number of inputs to read.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns>A list of <see cref="DiscreteInput"/>s.</returns>
|
||||
public virtual async Task<IReadOnlyList<DiscreteInput>> ReadDiscreteInputsAsync(byte unitId, ushort startAddress, ushort count, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeReadDiscreteInputs(unitId, startAddress, count);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
// The protocol processes complete bytes from the response.
|
||||
// So reduce to the actual discrete input count.
|
||||
var discreteInputs = Protocol.DeserializeReadDiscreteInputs(response).Take(count);
|
||||
foreach (var discreteInput in discreteInputs)
|
||||
discreteInput.Address += startAddress;
|
||||
|
||||
return discreteInputs.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads multiple <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="startAddress">The starting address.</param>
|
||||
/// <param name="count">The number of registers to read.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns>A list of <see cref="HoldingRegister"/>s.</returns>
|
||||
public virtual async Task<IReadOnlyList<HoldingRegister>> ReadHoldingRegistersAsync(byte unitId, ushort startAddress, ushort count, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeReadHoldingRegisters(unitId, startAddress, count);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
var holdingRegisters = Protocol.DeserializeReadHoldingRegisters(response).ToList();
|
||||
foreach (var holdingRegister in holdingRegisters)
|
||||
holdingRegister.Address += startAddress;
|
||||
|
||||
return holdingRegisters;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads multiple <see cref="InputRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="startAddress">The starting address.</param>
|
||||
/// <param name="count">The number of registers to read.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns>A list of <see cref="InputRegister"/>s.</returns>
|
||||
public virtual async Task<IReadOnlyList<InputRegister>> ReadInputRegistersAsync(byte unitId, ushort startAddress, ushort count, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeReadInputRegisters(unitId, startAddress, count);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
var inputRegisters = Protocol.DeserializeReadInputRegisters(response).ToList();
|
||||
foreach (var inputRegister in inputRegisters)
|
||||
inputRegister.Address += startAddress;
|
||||
|
||||
return inputRegisters;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a single <see cref="Coil"/>.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="coil">The coil to write.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns><see langword="true"/> on success, otherwise <see langword="false"/>.</returns>
|
||||
public virtual async Task<bool> WriteSingleCoilAsync(byte unitId, Coil coil, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeWriteSingleCoil(unitId, coil);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
var result = Protocol.DeserializeWriteSingleCoil(response);
|
||||
|
||||
return coil.Address == result.Address
|
||||
&& coil.Value == result.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writs a single <see cref="HoldingRegister"/>.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="register">The register to write.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns><see langword="true"/> on success, otherwise <see langword="false"/>.</returns>
|
||||
public virtual async Task<bool> WriteSingleHoldingRegisterAsync(byte unitId, HoldingRegister register, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeWriteSingleHoldingRegister(unitId, register);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
var result = Protocol.DeserializeWriteSingleHoldingRegister(response);
|
||||
|
||||
return register.Address == result.Address
|
||||
&& register.Value == result.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes multiple <see cref="Coil"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="coils">The coils to write.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns><see langword="true"/> on success, otherwise <see langword="false"/>.</returns>
|
||||
public virtual async Task<bool> WriteMultipleCoilsAsync(byte unitId, IReadOnlyList<Coil> coils, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeWriteMultipleCoils(unitId, coils);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
var (firstAddress, count) = Protocol.DeserializeWriteMultipleCoils(response);
|
||||
|
||||
return coils.Count == count && coils.OrderBy(c => c.Address).First().Address == firstAddress;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes multiple <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id.</param>
|
||||
/// <param name="registers">The registers to write.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns><see langword="true"/> on success, otherwise <see langword="false"/>.</returns>
|
||||
public virtual async Task<bool> WriteMultipleHoldingRegistersAsync(byte unitId, IReadOnlyList<HoldingRegister> registers, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeWriteMultipleHoldingRegisters(unitId, registers);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
var (firstAddress, count) = Protocol.DeserializeWriteMultipleHoldingRegisters(response);
|
||||
|
||||
return registers.Count == count && registers.OrderBy(c => c.Address).First().Address == firstAddress;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases all managed and unmanaged resources used by the <see cref="ModbusClientBase"/>.
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
=> $"Modbus client using {Protocol.Name} protocol to connect via {connection.Name}";
|
||||
|
||||
/// <summary>
|
||||
/// Releases the unmanaged resources used by the <see cref="ModbusClientBase"/>
|
||||
/// and optionally also discards the managed resources.
|
||||
/// </summary>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && !_isDisposed)
|
||||
{
|
||||
_isDisposed = true;
|
||||
|
||||
if (disposeConnection)
|
||||
connection.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs basic assertions.
|
||||
/// </summary>
|
||||
protected virtual void Assertions(bool checkConnected = true)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ObjectDisposedException.ThrowIf(_isDisposed, this);
|
||||
#else
|
||||
if (_isDisposed)
|
||||
throw new ObjectDisposedException(GetType().FullName);
|
||||
#endif
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(Protocol);
|
||||
#else
|
||||
if (Protocol == null)
|
||||
throw new ArgumentNullException(nameof(Protocol));
|
||||
#endif
|
||||
|
||||
if (!checkConnected)
|
||||
return;
|
||||
|
||||
if (!IsConnected)
|
||||
throw new ApplicationException($"Connection is not open");
|
||||
}
|
||||
}
|
||||
}
|
||||
76
AMWD.Protocols.Modbus.Common/Enums/ModbusErrorCode.cs
Normal file
76
AMWD.Protocols.Modbus.Common/Enums/ModbusErrorCode.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// List of Modbus exception codes.
|
||||
/// </summary>
|
||||
public enum ModbusErrorCode : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// No error.
|
||||
/// </summary>
|
||||
[Description("No error")]
|
||||
NoError = 0x00,
|
||||
|
||||
/// <summary>
|
||||
/// Function code not valid/supported.
|
||||
/// </summary>
|
||||
[Description("Illegal function")]
|
||||
IllegalFunction = 0x01,
|
||||
|
||||
/// <summary>
|
||||
/// Data address not in range.
|
||||
/// </summary>
|
||||
[Description("Illegal data address")]
|
||||
IllegalDataAddress = 0x02,
|
||||
|
||||
/// <summary>
|
||||
/// The data value to set is not valid.
|
||||
/// </summary>
|
||||
[Description("Illegal data value")]
|
||||
IllegalDataValue = 0x03,
|
||||
|
||||
/// <summary>
|
||||
/// Slave device produced a failure.
|
||||
/// </summary>
|
||||
[Description("Slave device failure")]
|
||||
SlaveDeviceFailure = 0x04,
|
||||
|
||||
/// <summary>
|
||||
/// Ack
|
||||
/// </summary>
|
||||
[Description("Acknowledge")]
|
||||
Acknowledge = 0x05,
|
||||
|
||||
/// <summary>
|
||||
/// Slave device is working on another task.
|
||||
/// </summary>
|
||||
[Description("Slave device busy")]
|
||||
SlaveDeviceBusy = 0x06,
|
||||
|
||||
/// <summary>
|
||||
/// nAck
|
||||
/// </summary>
|
||||
[Description("Negative acknowledge")]
|
||||
NegativeAcknowledge = 0x07,
|
||||
|
||||
/// <summary>
|
||||
/// Momory Parity Error.
|
||||
/// </summary>
|
||||
[Description("Memory parity error")]
|
||||
MemoryParityError = 0x08,
|
||||
|
||||
/// <summary>
|
||||
/// Gateway of the device could not be reached.
|
||||
/// </summary>
|
||||
[Description("Gateway path unavailable")]
|
||||
GatewayPath = 0x0A,
|
||||
|
||||
/// <summary>
|
||||
/// Gateway device did no respond.
|
||||
/// </summary>
|
||||
[Description("Gateway target device failed to respond")]
|
||||
GatewayTargetDevice = 0x0B
|
||||
}
|
||||
}
|
||||
67
AMWD.Protocols.Modbus.Common/Enums/ModbusFunctionCode.cs
Normal file
67
AMWD.Protocols.Modbus.Common/Enums/ModbusFunctionCode.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// List of the Modbus function codes.
|
||||
/// </summary>
|
||||
public enum ModbusFunctionCode : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Read coils (Fn 1).
|
||||
/// </summary>
|
||||
[Description("Read Coils")]
|
||||
ReadCoils = 0x01,
|
||||
|
||||
/// <summary>
|
||||
/// Read discrete inputs (Fn 2).
|
||||
/// </summary>
|
||||
[Description("Read Discrete Inputs")]
|
||||
ReadDiscreteInputs = 0x02,
|
||||
|
||||
/// <summary>
|
||||
/// Reads holding registers (Fn 3).
|
||||
/// </summary>
|
||||
[Description("Read Holding Registers")]
|
||||
ReadHoldingRegisters = 0x03,
|
||||
|
||||
/// <summary>
|
||||
/// Reads input registers (Fn 4).
|
||||
/// </summary>
|
||||
[Description("Read Input Registers")]
|
||||
ReadInputRegisters = 0x04,
|
||||
|
||||
/// <summary>
|
||||
/// Writes a single coil (Fn 5).
|
||||
/// </summary>
|
||||
[Description("Write Single Coil")]
|
||||
WriteSingleCoil = 0x05,
|
||||
|
||||
/// <summary>
|
||||
/// Writes a single register (Fn 6).
|
||||
/// </summary>
|
||||
[Description("Write Single Register")]
|
||||
WriteSingleRegister = 0x06,
|
||||
|
||||
/// <summary>
|
||||
/// Writes multiple coils (Fn 15).
|
||||
/// </summary>
|
||||
[Description("Write Multiple Coils")]
|
||||
WriteMultipleCoils = 0x0F,
|
||||
|
||||
/// <summary>
|
||||
/// Writes multiple registers (Fn 16).
|
||||
/// </summary>
|
||||
[Description("Write Multiple Registers")]
|
||||
WriteMultipleRegisters = 0x10,
|
||||
|
||||
/// <summary>
|
||||
/// Tunnels service requests and method invocations (Fn 43).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This function code needs additional information about its type of request.
|
||||
/// </remarks>
|
||||
[Description("MODBUS Encapsulated Interface (MEI)")]
|
||||
EncapsulatedInterface = 0x2B
|
||||
}
|
||||
}
|
||||
28
AMWD.Protocols.Modbus.Common/Enums/ModbusObjectType.cs
Normal file
28
AMWD.Protocols.Modbus.Common/Enums/ModbusObjectType.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// List of specific types.
|
||||
/// </summary>
|
||||
public enum ModbusObjectType
|
||||
{
|
||||
/// <summary>
|
||||
/// The discrete value is a coil (read/write).
|
||||
/// </summary>
|
||||
Coil = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The discrete value is an input (read only).
|
||||
/// </summary>
|
||||
DiscreteInput = 2,
|
||||
|
||||
/// <summary>
|
||||
/// The value is a holding register (read/write).
|
||||
/// </summary>
|
||||
HoldingRegister = 3,
|
||||
|
||||
/// <summary>
|
||||
/// The value is an input register (read only).
|
||||
/// </summary>
|
||||
InputRegister = 4
|
||||
}
|
||||
}
|
||||
77
AMWD.Protocols.Modbus.Common/Exceptions/ModbusException.cs
Normal file
77
AMWD.Protocols.Modbus.Common/Exceptions/ModbusException.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents errors that occurr during Modbus requests.
|
||||
/// </summary>
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class ModbusException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModbusException"/> class.
|
||||
/// </summary>
|
||||
public ModbusException()
|
||||
: base()
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModbusException"/> class
|
||||
/// with a specified error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
public ModbusException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModbusException"/> class
|
||||
/// with a specified error message and a reference to the inner exception
|
||||
/// that is the cause of this exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
/// <param name="innerException">
|
||||
/// The exception that is the cause of the current exception,
|
||||
/// or a null reference if no inner exception is specified.
|
||||
/// </param>
|
||||
public ModbusException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
|
||||
#if !NET8_0_OR_GREATER
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModbusException"/> class
|
||||
/// with serialized data.
|
||||
/// </summary>
|
||||
/// <param name="info">
|
||||
/// The <see cref="SerializationInfo"/> that holds the serialized
|
||||
/// object data about the exception being thrown.
|
||||
/// </param>
|
||||
/// <param name="context">
|
||||
/// The <see cref="StreamingContext"/> that contains contextual
|
||||
/// information about the source or destination.
|
||||
/// </param>
|
||||
protected ModbusException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{ }
|
||||
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Modubs error code.
|
||||
/// </summary>
|
||||
#if NET6_0_OR_GREATER
|
||||
public ModbusErrorCode ErrorCode { get; init; }
|
||||
#else
|
||||
public ModbusErrorCode ErrorCode { get; set; }
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Modbus error message.
|
||||
/// </summary>
|
||||
public string ErrorMessage => ErrorCode.GetDescription();
|
||||
}
|
||||
}
|
||||
14
AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs
Normal file
14
AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal static class ArrayExtensions
|
||||
{
|
||||
public static void SwapNetworkOrder(this byte[] bytes)
|
||||
{
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
AMWD.Protocols.Modbus.Common/Extensions/EnumExtensions.cs
Normal file
30
AMWD.Protocols.Modbus.Common/Extensions/EnumExtensions.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace System
|
||||
{
|
||||
// ================================================================================================================================== //
|
||||
// Source: https://git.am-wd.de/am.wd/common/-/blob/d4b390ad911ce302cc371bb2121fa9c31db1674a/AMWD.Common/Extensions/EnumExtensions.cs //
|
||||
// ================================================================================================================================== //
|
||||
[Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal static class EnumExtensions
|
||||
{
|
||||
private static IEnumerable<TAttribute> GetAttributes<TAttribute>(this Enum value)
|
||||
where TAttribute : Attribute
|
||||
{
|
||||
var fieldInfo = value.GetType().GetField(value.ToString());
|
||||
if (fieldInfo == null)
|
||||
return Array.Empty<TAttribute>();
|
||||
|
||||
return fieldInfo.GetCustomAttributes(typeof(TAttribute), inherit: false).Cast<TAttribute>();
|
||||
}
|
||||
|
||||
private static TAttribute GetAttribute<TAttribute>(this Enum value)
|
||||
where TAttribute : Attribute
|
||||
=> value.GetAttributes<TAttribute>().FirstOrDefault();
|
||||
|
||||
public static string GetDescription(this Enum value)
|
||||
=> value.GetAttribute<DescriptionAttribute>()?.Description ?? value.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom extensions for <see cref="ModbusObject"/>s.
|
||||
/// </summary>
|
||||
public static class ModbusDecimalExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts multiple <see cref="ModbusObject"/>s into a <see cref="float"/> value.
|
||||
/// </summary>
|
||||
/// <param name="list">The list of Modbus objects.</param>
|
||||
/// <param name="startIndex">The first index to use.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The objects float value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the list is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the list is too short or the list contains mixed/incompatible objects.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">when the <paramref name="startIndex"/> is too high.</exception>
|
||||
public static float GetSingle(this IEnumerable<ModbusObject> list, int startIndex = 0, bool reverseRegisterOrder = false)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(list);
|
||||
#else
|
||||
if (list == null)
|
||||
throw new ArgumentNullException(nameof(list));
|
||||
#endif
|
||||
|
||||
int count = list.Count();
|
||||
if (count < 2)
|
||||
throw new ArgumentException("At least two registers required", nameof(list));
|
||||
|
||||
if (startIndex < 0 || startIndex + 2 > count)
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex));
|
||||
|
||||
if (!list.All(o => o.Type == ModbusObjectType.HoldingRegister) && !list.All(o => o.Type == ModbusObjectType.InputRegister))
|
||||
throw new ArgumentException("Mixed object typs found", nameof(list));
|
||||
|
||||
var registers = list.OrderBy(o => o.Address).Skip(startIndex).Take(2).ToArray();
|
||||
if (reverseRegisterOrder)
|
||||
Array.Reverse(registers);
|
||||
|
||||
byte[] blob = new byte[registers.Length * 2];
|
||||
for (int i = 0; i < registers.Length; i++)
|
||||
{
|
||||
blob[i * 2] = registers[i].HighByte;
|
||||
blob[i * 2 + 1] = registers[i].LowByte;
|
||||
}
|
||||
|
||||
blob.SwapNetworkOrder();
|
||||
return BitConverter.ToSingle(blob, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts multiple <see cref="ModbusObject"/>s into a <see cref="double"/> value.
|
||||
/// </summary>
|
||||
/// <param name="list">The list of Modbus objects.</param>
|
||||
/// <param name="startIndex">The first index to use.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The objects double value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the list is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the list is too short or the list contains mixed/incompatible objects.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">when the <paramref name="startIndex"/> is too high.</exception>
|
||||
public static double GetDouble(this IEnumerable<ModbusObject> list, int startIndex = 0, bool reverseRegisterOrder = false)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(list);
|
||||
#else
|
||||
if (list == null)
|
||||
throw new ArgumentNullException(nameof(list));
|
||||
#endif
|
||||
|
||||
int count = list.Count();
|
||||
if (count < 4)
|
||||
throw new ArgumentException("At least four registers required", nameof(list));
|
||||
|
||||
if (startIndex < 0 || startIndex + 4 > count)
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex));
|
||||
|
||||
if (!list.All(o => o.Type == ModbusObjectType.HoldingRegister) && !list.All(o => o.Type == ModbusObjectType.InputRegister))
|
||||
throw new ArgumentException("Mixed object typs found", nameof(list));
|
||||
|
||||
var registers = list.OrderBy(o => o.Address).Skip(startIndex).Take(4).ToArray();
|
||||
if (reverseRegisterOrder)
|
||||
Array.Reverse(registers);
|
||||
|
||||
byte[] blob = new byte[registers.Length * 2];
|
||||
for (int i = 0; i < registers.Length; i++)
|
||||
{
|
||||
blob[i * 2] = registers[i].HighByte;
|
||||
blob[i * 2 + 1] = registers[i].LowByte;
|
||||
}
|
||||
|
||||
blob.SwapNetworkOrder();
|
||||
return BitConverter.ToDouble(blob, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="float"/> value to a list of <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="value">The float value.</param>
|
||||
/// <param name="address">The first register address.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The list of registers.</returns>
|
||||
public static IEnumerable<HoldingRegister> ToRegister(this float value, ushort address, bool reverseRegisterOrder = false)
|
||||
{
|
||||
byte[] blob = BitConverter.GetBytes(value);
|
||||
blob.SwapNetworkOrder();
|
||||
|
||||
int numRegisters = blob.Length / 2;
|
||||
for (int i = 0; i < numRegisters; i++)
|
||||
{
|
||||
int addr = reverseRegisterOrder
|
||||
? address + numRegisters - 1 - i
|
||||
: address + i;
|
||||
|
||||
yield return new HoldingRegister
|
||||
{
|
||||
Address = (ushort)addr,
|
||||
HighByte = blob[i * 2],
|
||||
LowByte = blob[i * 2 + 1]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="double"/> value to a list of <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="value">The double value.</param>
|
||||
/// <param name="address">The first register address.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The list of registers.</returns>
|
||||
public static IEnumerable<HoldingRegister> ToRegister(this double value, ushort address, bool reverseRegisterOrder = false)
|
||||
{
|
||||
byte[] blob = BitConverter.GetBytes(value);
|
||||
blob.SwapNetworkOrder();
|
||||
|
||||
int numRegisters = blob.Length / 2;
|
||||
for (int i = 0; i < numRegisters; i++)
|
||||
{
|
||||
int addr = reverseRegisterOrder
|
||||
? address + numRegisters - 1 - i
|
||||
: address + i;
|
||||
|
||||
yield return new HoldingRegister
|
||||
{
|
||||
Address = (ushort)addr,
|
||||
HighByte = blob[i * 2],
|
||||
LowByte = blob[i * 2 + 1]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
175
AMWD.Protocols.Modbus.Common/Extensions/ModbusExtensions.cs
Normal file
175
AMWD.Protocols.Modbus.Common/Extensions/ModbusExtensions.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom extensions for <see cref="ModbusObject"/>s.
|
||||
/// </summary>
|
||||
public static class ModbusExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a <see cref="ModbusObject"/> into a <see cref="bool"/> value.
|
||||
/// </summary>
|
||||
/// <param name="obj">The Modbus object.</param>
|
||||
/// <returns>The objects bool value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the object is null.</exception>
|
||||
public static bool GetBoolean(this ModbusObject obj)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(obj);
|
||||
#else
|
||||
if (obj == null)
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
#endif
|
||||
|
||||
if (obj is Coil coil)
|
||||
return coil.Value;
|
||||
|
||||
if (obj is DiscreteInput discreteInput)
|
||||
return discreteInput.Value;
|
||||
|
||||
return obj.HighByte > 0 || obj.LowByte > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts multiple <see cref="ModbusObject"/>s into a <see cref="string"/> value.
|
||||
/// </summary>
|
||||
/// <param name="list">The list of Modbus objects.</param>
|
||||
/// <param name="length">The number of registers to use.</param>
|
||||
/// <param name="startIndex">The first index to use.</param>
|
||||
/// <param name="encoding">The encoding used to convert the text. (Default: <see cref="Encoding.ASCII"/>)</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <param name="reverseByteOrderPerRegister">Indicates whether to reverse high and low byte per register.</param>
|
||||
/// <returns>The objects text value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the list is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the list is too short or the list contains mixed/incompatible objects.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">when the <paramref name="startIndex"/> is too high.</exception>
|
||||
public static string GetString(this IEnumerable<ModbusObject> list, int length, int startIndex = 0, Encoding encoding = null, bool reverseRegisterOrder = false, bool reverseByteOrderPerRegister = false)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(list);
|
||||
#else
|
||||
if (list == null)
|
||||
throw new ArgumentNullException(nameof(list));
|
||||
#endif
|
||||
|
||||
int count = list.Count();
|
||||
if (count < length)
|
||||
throw new ArgumentException($"At least {length} registers required", nameof(list));
|
||||
|
||||
if (startIndex < 0 || startIndex + length > count)
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex));
|
||||
|
||||
if (!list.All(o => o.Type == ModbusObjectType.HoldingRegister) && !list.All(o => o.Type == ModbusObjectType.InputRegister))
|
||||
throw new ArgumentException("Mixed object types found", nameof(list));
|
||||
|
||||
var registers = list.OrderBy(o => o.Address).Skip(startIndex).Take(length).ToArray();
|
||||
if (reverseRegisterOrder)
|
||||
Array.Reverse(registers);
|
||||
|
||||
byte[] blob = new byte[registers.Length * 2];
|
||||
for (int i = 0; i < registers.Length; i++)
|
||||
{
|
||||
blob[i * 2] = reverseByteOrderPerRegister
|
||||
? registers[i].LowByte
|
||||
: registers[i].HighByte;
|
||||
|
||||
blob[i * 2 + 1] = reverseByteOrderPerRegister
|
||||
? registers[i].HighByte
|
||||
: registers[i].LowByte;
|
||||
}
|
||||
|
||||
string text = (encoding ?? Encoding.ASCII).GetString(blob).Trim([' ', '\t', '\0', '\r', '\n']);
|
||||
int nullIndex = text.IndexOf('\0');
|
||||
|
||||
if (nullIndex > 0)
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
return text[..nullIndex];
|
||||
#else
|
||||
return text.Substring(0, nullIndex);
|
||||
#endif
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="bool"/> value to a <see cref="Coil"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The bool value.</param>
|
||||
/// <param name="address">The coil address.</param>
|
||||
/// <returns>The coil.</returns>
|
||||
public static Coil ToCoil(this bool value, ushort address)
|
||||
{
|
||||
return new Coil
|
||||
{
|
||||
Address = address,
|
||||
Value = value
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="bool"/> value to a <see cref="HoldingRegister"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The bool value.</param>
|
||||
/// <param name="address">The register address.</param>
|
||||
/// <returns>The register.</returns>
|
||||
public static HoldingRegister ToRegister(this bool value, ushort address)
|
||||
{
|
||||
return new HoldingRegister
|
||||
{
|
||||
Address = address,
|
||||
Value = (ushort)(value ? 1 : 0)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="string"/> value to a <see cref="HoldingRegister"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The text.</param>
|
||||
/// <param name="address">The address of the text.</param>
|
||||
/// <param name="encoding">The encoding used to convert the text. (Default: <see cref="Encoding.ASCII"/>)</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <param name="reverseByteOrderPerRegister">Indicates whether to reverse high and low byte per register.</param>
|
||||
/// <returns>The registers.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the text is null.</exception>
|
||||
public static IEnumerable<HoldingRegister> ToRegisters(this string value, ushort address, Encoding encoding = null, bool reverseRegisterOrder = false, bool reverseByteOrderPerRegister = false)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
#else
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
#endif
|
||||
|
||||
byte[] blob = (encoding ?? Encoding.ASCII).GetBytes(value);
|
||||
int numRegisters = (int)Math.Ceiling(blob.Length / 2.0);
|
||||
|
||||
for (int i = 0; i < numRegisters; i++)
|
||||
{
|
||||
int addr = reverseRegisterOrder
|
||||
? address + numRegisters - 1 - i
|
||||
: address + i;
|
||||
|
||||
var register = new HoldingRegister
|
||||
{
|
||||
Address = (ushort)addr,
|
||||
|
||||
HighByte = reverseByteOrderPerRegister
|
||||
? (i * 2 + 1 < blob.Length ? blob[i * 2 + 1] : (byte)0)
|
||||
: blob[i * 2],
|
||||
|
||||
LowByte = reverseByteOrderPerRegister
|
||||
? blob[i * 2]
|
||||
: (i * 2 + 1 < blob.Length ? blob[i * 2 + 1] : (byte)0)
|
||||
};
|
||||
|
||||
yield return register;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom extensions for <see cref="ModbusObject"/>s.
|
||||
/// </summary>
|
||||
public static class ModbusSignedExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a <see cref="ModbusObject"/> into a <see cref="sbyte"/> value.
|
||||
/// </summary>
|
||||
/// <param name="obj">The Modbus object.</param>
|
||||
/// <returns>The objects signed byte value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the object is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the wrong types are provided.</exception>
|
||||
public static sbyte GetSByte(this ModbusObject obj)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(obj);
|
||||
#else
|
||||
if (obj == null)
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
#endif
|
||||
|
||||
if (obj is HoldingRegister holdingRegister)
|
||||
return (sbyte)holdingRegister.Value;
|
||||
|
||||
if (obj is InputRegister inputRegister)
|
||||
return (sbyte)inputRegister.Value;
|
||||
|
||||
throw new ArgumentException($"The object type '{obj.GetType()}' is invalid", nameof(obj));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="ModbusObject"/> into a <see cref="short"/> value.
|
||||
/// </summary>
|
||||
/// <param name="obj">The Modbus object.</param>
|
||||
/// <returns>The objects short value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the object is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the wrong types are provided.</exception>
|
||||
public static short GetInt16(this ModbusObject obj)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(obj);
|
||||
#else
|
||||
if (obj == null)
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
#endif
|
||||
|
||||
if (obj is HoldingRegister holdingRegister)
|
||||
return (short)holdingRegister.Value;
|
||||
|
||||
if (obj is InputRegister inputRegister)
|
||||
return (short)inputRegister.Value;
|
||||
|
||||
throw new ArgumentException($"The object type '{obj.GetType()}' is invalid", nameof(obj));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts multiple <see cref="ModbusObject"/>s into a <see cref="int"/> value.
|
||||
/// </summary>
|
||||
/// <param name="list">The list of Modbus objects.</param>
|
||||
/// <param name="startIndex">The first index to use.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The objects int value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the list is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the list is too short or the list contains mixed/incompatible objects.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">when the <paramref name="startIndex"/> is too high.</exception>
|
||||
public static int GetInt32(this IEnumerable<ModbusObject> list, int startIndex = 0, bool reverseRegisterOrder = false)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(list);
|
||||
#else
|
||||
if (list == null)
|
||||
throw new ArgumentNullException(nameof(list));
|
||||
#endif
|
||||
|
||||
int count = list.Count();
|
||||
if (count < 2)
|
||||
throw new ArgumentException("At least two registers required", nameof(list));
|
||||
|
||||
if (startIndex < 0 || startIndex + 2 > count)
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex));
|
||||
|
||||
if (!list.All(o => o.Type == ModbusObjectType.HoldingRegister) && !list.All(o => o.Type == ModbusObjectType.InputRegister))
|
||||
throw new ArgumentException("Mixed object typs found", nameof(list));
|
||||
|
||||
var registers = list.OrderBy(o => o.Address).Skip(startIndex).Take(2).ToArray();
|
||||
if (reverseRegisterOrder)
|
||||
Array.Reverse(registers);
|
||||
|
||||
byte[] blob = new byte[registers.Length * 2];
|
||||
for (int i = 0; i < registers.Length; i++)
|
||||
{
|
||||
blob[i * 2] = registers[i].HighByte;
|
||||
blob[i * 2 + 1] = registers[i].LowByte;
|
||||
}
|
||||
|
||||
blob.SwapNetworkOrder();
|
||||
return BitConverter.ToInt32(blob, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts multiple <see cref="ModbusObject"/>s into a <see cref="long"/> value.
|
||||
/// </summary>
|
||||
/// <param name="list">The list of Modbus objects.</param>
|
||||
/// <param name="startIndex">The first index to use.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The objects long value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the list is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the list is too short or the list contains mixed/incompatible objects.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">when the <paramref name="startIndex"/> is too high.</exception>
|
||||
public static long GetInt64(this IEnumerable<ModbusObject> list, int startIndex = 0, bool reverseRegisterOrder = false)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(list);
|
||||
#else
|
||||
if (list == null)
|
||||
throw new ArgumentNullException(nameof(list));
|
||||
#endif
|
||||
|
||||
int count = list.Count();
|
||||
if (count < 4)
|
||||
throw new ArgumentException("At least four registers required", nameof(list));
|
||||
|
||||
if (startIndex < 0 || startIndex + 4 > count)
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex));
|
||||
|
||||
if (!list.All(o => o.Type == ModbusObjectType.HoldingRegister) && !list.All(o => o.Type == ModbusObjectType.InputRegister))
|
||||
throw new ArgumentException("Mixed object typs found", nameof(list));
|
||||
|
||||
var registers = list.OrderBy(o => o.Address).Skip(startIndex).Take(4).ToArray();
|
||||
if (reverseRegisterOrder)
|
||||
Array.Reverse(registers);
|
||||
|
||||
byte[] blob = new byte[registers.Length * 2];
|
||||
for (int i = 0; i < registers.Length; i++)
|
||||
{
|
||||
blob[i * 2] = registers[i].HighByte;
|
||||
blob[i * 2 + 1] = registers[i].LowByte;
|
||||
}
|
||||
|
||||
blob.SwapNetworkOrder();
|
||||
return BitConverter.ToInt64(blob, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="sbyte"/> value to a <see cref="HoldingRegister"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The signed byte value.</param>
|
||||
/// <param name="address">The register address.</param>
|
||||
/// <returns>The register.</returns>
|
||||
public static HoldingRegister ToRegister(this sbyte value, ushort address)
|
||||
{
|
||||
return new HoldingRegister
|
||||
{
|
||||
Address = address,
|
||||
LowByte = (byte)value
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="short"/> value to a <see cref="HoldingRegister"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The short value.</param>
|
||||
/// <param name="address">The register address.</param>
|
||||
/// <returns>The register.</returns>
|
||||
public static HoldingRegister ToRegister(this short value, ushort address)
|
||||
{
|
||||
byte[] blob = BitConverter.GetBytes(value);
|
||||
blob.SwapNetworkOrder();
|
||||
|
||||
return new HoldingRegister
|
||||
{
|
||||
Address = address,
|
||||
HighByte = blob[0],
|
||||
LowByte = blob[1]
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="int"/> value to a list of <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="value">The int value.</param>
|
||||
/// <param name="address">The first register address.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The list of registers.</returns>
|
||||
public static IEnumerable<HoldingRegister> ToRegister(this int value, ushort address, bool reverseRegisterOrder = false)
|
||||
{
|
||||
byte[] blob = BitConverter.GetBytes(value);
|
||||
blob.SwapNetworkOrder();
|
||||
|
||||
int numRegisters = blob.Length / 2;
|
||||
for (int i = 0; i < numRegisters; i++)
|
||||
{
|
||||
int addr = reverseRegisterOrder
|
||||
? address + numRegisters - 1 - i
|
||||
: address + i;
|
||||
|
||||
yield return new HoldingRegister
|
||||
{
|
||||
Address = (ushort)addr,
|
||||
HighByte = blob[i * 2],
|
||||
LowByte = blob[i * 2 + 1]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="long"/> value to a list of <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="value">The long value.</param>
|
||||
/// <param name="address">The first register address.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The list of registers.</returns>
|
||||
public static IEnumerable<HoldingRegister> ToRegister(this long value, ushort address, bool reverseRegisterOrder = false)
|
||||
{
|
||||
byte[] blob = BitConverter.GetBytes(value);
|
||||
blob.SwapNetworkOrder();
|
||||
|
||||
int numRegisters = blob.Length / 2;
|
||||
for (int i = 0; i < numRegisters; i++)
|
||||
{
|
||||
int addr = reverseRegisterOrder
|
||||
? address + numRegisters - 1 - i
|
||||
: address + i;
|
||||
|
||||
yield return new HoldingRegister
|
||||
{
|
||||
Address = (ushort)addr,
|
||||
HighByte = blob[i * 2],
|
||||
LowByte = blob[i * 2 + 1]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom extensions for <see cref="ModbusObject"/>s.
|
||||
/// </summary>
|
||||
public static class ModbusUnsignedExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a <see cref="ModbusObject"/> into a <see cref="byte"/> value.
|
||||
/// </summary>
|
||||
/// <param name="obj">The Modbus object.</param>
|
||||
/// <returns>The objects byte value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the object is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the wrong types are provided.</exception>
|
||||
public static byte GetByte(this ModbusObject obj)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(obj);
|
||||
#else
|
||||
if (obj == null)
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
#endif
|
||||
|
||||
if (obj is HoldingRegister holdingRegister)
|
||||
return (byte)holdingRegister.Value;
|
||||
|
||||
if (obj is InputRegister inputRegister)
|
||||
return (byte)inputRegister.Value;
|
||||
|
||||
throw new ArgumentException($"The object type '{obj.GetType()}' is invalid", nameof(obj));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="ModbusObject"/> into a <see cref="ushort"/> value.
|
||||
/// </summary>
|
||||
/// <param name="obj">The Modbus object.</param>
|
||||
/// <returns>The objects unsigned short value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the object is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the wrong types are provided.</exception>
|
||||
public static ushort GetUInt16(this ModbusObject obj)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(obj);
|
||||
#else
|
||||
if (obj == null)
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
#endif
|
||||
|
||||
if (obj is HoldingRegister holdingRegister)
|
||||
return holdingRegister.Value;
|
||||
|
||||
if (obj is InputRegister inputRegister)
|
||||
return inputRegister.Value;
|
||||
|
||||
throw new ArgumentException($"The object type '{obj.GetType()}' is invalid", nameof(obj));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts multiple <see cref="ModbusObject"/>s into a <see cref="uint"/> value.
|
||||
/// </summary>
|
||||
/// <param name="list">The list of Modbus objects.</param>
|
||||
/// <param name="startIndex">The first index to use.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The objects unsigned int value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the list is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the list is too short or the list contains mixed/incompatible objects.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">when the <paramref name="startIndex"/> is too high.</exception>
|
||||
public static uint GetUInt32(this IEnumerable<ModbusObject> list, int startIndex = 0, bool reverseRegisterOrder = false)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(list);
|
||||
#else
|
||||
if (list == null)
|
||||
throw new ArgumentNullException(nameof(list));
|
||||
#endif
|
||||
|
||||
int count = list.Count();
|
||||
if (count < 2)
|
||||
throw new ArgumentException("At least two registers required", nameof(list));
|
||||
|
||||
if (startIndex < 0 || startIndex + 2 > count)
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex));
|
||||
|
||||
if (!list.All(o => o.Type == ModbusObjectType.HoldingRegister) && !list.All(o => o.Type == ModbusObjectType.InputRegister))
|
||||
throw new ArgumentException("Mixed object typs found", nameof(list));
|
||||
|
||||
var registers = list.OrderBy(o => o.Address).Skip(startIndex).Take(2).ToArray();
|
||||
if (reverseRegisterOrder)
|
||||
Array.Reverse(registers);
|
||||
|
||||
byte[] blob = new byte[registers.Length * 2];
|
||||
for (int i = 0; i < registers.Length; i++)
|
||||
{
|
||||
blob[i * 2] = registers[i].HighByte;
|
||||
blob[i * 2 + 1] = registers[i].LowByte;
|
||||
}
|
||||
|
||||
blob.SwapNetworkOrder();
|
||||
return BitConverter.ToUInt32(blob, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts multiple <see cref="ModbusObject"/>s into a <see cref="ulong"/> value.
|
||||
/// </summary>
|
||||
/// <param name="list">The list of Modbus objects.</param>
|
||||
/// <param name="startIndex">The first index to use.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The objects unsigned long value.</returns>
|
||||
/// <exception cref="ArgumentNullException">when the list is null.</exception>
|
||||
/// <exception cref="ArgumentException">when the list is too short or the list contains mixed/incompatible objects.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">when the <paramref name="startIndex"/> is too high.</exception>
|
||||
public static ulong GetUInt64(this IEnumerable<ModbusObject> list, int startIndex = 0, bool reverseRegisterOrder = false)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(list);
|
||||
#else
|
||||
if (list == null)
|
||||
throw new ArgumentNullException(nameof(list));
|
||||
#endif
|
||||
|
||||
int count = list.Count();
|
||||
if (count < 4)
|
||||
throw new ArgumentException("At least four registers required", nameof(list));
|
||||
|
||||
if (startIndex < 0 || startIndex + 4 > count)
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex));
|
||||
|
||||
if (!list.All(o => o.Type == ModbusObjectType.HoldingRegister) && !list.All(o => o.Type == ModbusObjectType.InputRegister))
|
||||
throw new ArgumentException("Mixed object typs found", nameof(list));
|
||||
|
||||
var registers = list.OrderBy(o => o.Address).Skip(startIndex).Take(4).ToArray();
|
||||
if (reverseRegisterOrder)
|
||||
Array.Reverse(registers);
|
||||
|
||||
byte[] blob = new byte[registers.Length * 2];
|
||||
for (int i = 0; i < registers.Length; i++)
|
||||
{
|
||||
blob[i * 2] = registers[i].HighByte;
|
||||
blob[i * 2 + 1] = registers[i].LowByte;
|
||||
}
|
||||
|
||||
blob.SwapNetworkOrder();
|
||||
return BitConverter.ToUInt64(blob, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="byte"/> value to a <see cref="HoldingRegister"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The byte value.</param>
|
||||
/// <param name="address">The register address.</param>
|
||||
/// <returns>The register.</returns>
|
||||
public static HoldingRegister ToRegister(this byte value, ushort address)
|
||||
{
|
||||
return new HoldingRegister
|
||||
{
|
||||
Address = address,
|
||||
LowByte = value
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="ushort"/> value to a <see cref="HoldingRegister"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The unsigned short value.</param>
|
||||
/// <param name="address">The register address.</param>
|
||||
/// <returns>The register.</returns>
|
||||
public static HoldingRegister ToRegister(this ushort value, ushort address)
|
||||
{
|
||||
byte[] blob = BitConverter.GetBytes(value);
|
||||
blob.SwapNetworkOrder();
|
||||
|
||||
return new HoldingRegister
|
||||
{
|
||||
Address = address,
|
||||
HighByte = blob[0],
|
||||
LowByte = blob[1]
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="uint"/> value to a list of <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="value">The unsigned int value.</param>
|
||||
/// <param name="address">The first register address.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The list of registers.</returns>
|
||||
public static IEnumerable<HoldingRegister> ToRegister(this uint value, ushort address, bool reverseRegisterOrder = false)
|
||||
{
|
||||
byte[] blob = BitConverter.GetBytes(value);
|
||||
blob.SwapNetworkOrder();
|
||||
|
||||
int numRegisters = blob.Length / 2;
|
||||
for (int i = 0; i < numRegisters; i++)
|
||||
{
|
||||
int addr = reverseRegisterOrder
|
||||
? address + numRegisters - 1 - i
|
||||
: address + i;
|
||||
|
||||
yield return new HoldingRegister
|
||||
{
|
||||
Address = (ushort)addr,
|
||||
HighByte = blob[i * 2],
|
||||
LowByte = blob[i * 2 + 1]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="ulong"/> value to a list of <see cref="HoldingRegister"/>s.
|
||||
/// </summary>
|
||||
/// <param name="value">The unsigned long value.</param>
|
||||
/// <param name="address">The first register address.</param>
|
||||
/// <param name="reverseRegisterOrder">Indicates whehter the taken registers should be reversed.</param>
|
||||
/// <returns>The list of registers.</returns>
|
||||
public static IEnumerable<HoldingRegister> ToRegister(this ulong value, ushort address, bool reverseRegisterOrder = false)
|
||||
{
|
||||
byte[] blob = BitConverter.GetBytes(value);
|
||||
blob.SwapNetworkOrder();
|
||||
|
||||
int numRegisters = blob.Length / 2;
|
||||
for (int i = 0; i < numRegisters; i++)
|
||||
{
|
||||
int addr = reverseRegisterOrder
|
||||
? address + numRegisters - 1 - i
|
||||
: address + i;
|
||||
|
||||
yield return new HoldingRegister
|
||||
{
|
||||
Address = (ushort)addr,
|
||||
HighByte = blob[i * 2],
|
||||
LowByte = blob[i * 2 + 1]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
AMWD.Protocols.Modbus.Common/InternalsVisibleTo.cs
Normal file
3
AMWD.Protocols.Modbus.Common/InternalsVisibleTo.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("AMWD.Protocols.Modbus.Tests")]
|
||||
28
AMWD.Protocols.Modbus.Common/Models/Coil.cs
Normal file
28
AMWD.Protocols.Modbus.Common/Models/Coil.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a coil.
|
||||
/// </summary>
|
||||
public class Coil : ModbusObject
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ModbusObjectType Type => ModbusObjectType.Coil;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the coil is on or off.
|
||||
/// </summary>
|
||||
public bool Value
|
||||
{
|
||||
get => HighByte == 0xFF;
|
||||
set
|
||||
{
|
||||
HighByte = (byte)(value ? 0xFF : 0x00);
|
||||
LowByte = 0x00;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
=> $"Coil #{Address} | {(Value ? "ON" : "OFF")}";
|
||||
}
|
||||
}
|
||||
20
AMWD.Protocols.Modbus.Common/Models/DiscreteInput.cs
Normal file
20
AMWD.Protocols.Modbus.Common/Models/DiscreteInput.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a discrete input.
|
||||
/// </summary>
|
||||
public class DiscreteInput : ModbusObject
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ModbusObjectType Type => ModbusObjectType.DiscreteInput;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the discrete input is on or off.
|
||||
/// </summary>
|
||||
public bool Value => HighByte == 0xFF;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
=> $"Discrete Input #{Address} | {(Value ? "ON" : "OFF")}";
|
||||
}
|
||||
}
|
||||
41
AMWD.Protocols.Modbus.Common/Models/HoldingRegister.cs
Normal file
41
AMWD.Protocols.Modbus.Common/Models/HoldingRegister.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a holding register.
|
||||
/// </summary>
|
||||
public class HoldingRegister : ModbusObject
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ModbusObjectType Type => ModbusObjectType.HoldingRegister;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the holding register.
|
||||
/// </summary>
|
||||
public ushort Value
|
||||
{
|
||||
get
|
||||
{
|
||||
byte[] blob = [HighByte, LowByte];
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(blob);
|
||||
|
||||
return BitConverter.ToUInt16(blob, 0);
|
||||
}
|
||||
set
|
||||
{
|
||||
byte[] blob = BitConverter.GetBytes(value);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(blob);
|
||||
|
||||
HighByte = blob[0];
|
||||
LowByte = blob[1];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
=> $"Holding Register #{Address} | {Value} | HI: {HighByte:X2}, LO: {LowByte:X2}";
|
||||
}
|
||||
}
|
||||
32
AMWD.Protocols.Modbus.Common/Models/InputRegister.cs
Normal file
32
AMWD.Protocols.Modbus.Common/Models/InputRegister.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a input register.
|
||||
/// </summary>
|
||||
public class InputRegister : ModbusObject
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ModbusObjectType Type => ModbusObjectType.InputRegister;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the input register.
|
||||
/// </summary>
|
||||
public ushort Value
|
||||
{
|
||||
get
|
||||
{
|
||||
byte[] blob = [HighByte, LowByte];
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(blob);
|
||||
|
||||
return BitConverter.ToUInt16(blob, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
=> $"Input Register #{Address} | {Value} | HI: {HighByte:X2}, LO: {LowByte:X2}";
|
||||
}
|
||||
}
|
||||
56
AMWD.Protocols.Modbus.Common/Models/ModbusObject.cs
Normal file
56
AMWD.Protocols.Modbus.Common/Models/ModbusObject.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the base of all Modbus specific objects.
|
||||
/// </summary>
|
||||
public abstract class ModbusObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the type of the object.
|
||||
/// </summary>
|
||||
public abstract ModbusObjectType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the address of the object.
|
||||
/// </summary>
|
||||
public ushort Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the high byte of the value.
|
||||
/// </summary>
|
||||
public byte HighByte { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the low byte of the value.
|
||||
/// </summary>
|
||||
public byte LowByte { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is not ModbusObject mo)
|
||||
return false;
|
||||
|
||||
return Type == mo.Type
|
||||
&& Address == mo.Address
|
||||
&& HighByte == mo.HighByte
|
||||
&& LowByte == mo.LowByte;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public override int GetHashCode()
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
return HashCode.Combine(Type, Address, HighByte, LowByte);
|
||||
#else
|
||||
return Type.GetHashCode()
|
||||
^ Address.GetHashCode()
|
||||
^ HighByte.GetHashCode()
|
||||
^ LowByte.GetHashCode();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
646
AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs
Normal file
646
AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs
Normal file
@@ -0,0 +1,646 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
{
|
||||
/// <summary>
|
||||
/// Default implementation of the Modbus TCP protocol.
|
||||
/// </summary>
|
||||
public class TcpProtocol : IModbusProtocol
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly object _lock = new();
|
||||
private ushort _transactionId = 0x0000;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Constants
|
||||
|
||||
/// <summary>
|
||||
/// The minimum allowed unit id specified by the Modbus TCP protocol.
|
||||
/// </summary>
|
||||
public const byte MIN_UNIT_ID = 0x00;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum allowed unit id specified by the Modbus TCP protocol.
|
||||
/// </summary>
|
||||
public const byte MAX_UNIT_ID = 0xFF;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum allowed read count specified by the Modbus TCP protocol.
|
||||
/// </summary>
|
||||
public const ushort MIN_READ_COUNT = 0x01;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum allowed write count specified by the Modbus TCP protocol.
|
||||
/// </summary>
|
||||
public const ushort MIN_WRITE_COUNT = 0x01;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum allowed read count for discrete values specified by the Modbus TCP protocol.
|
||||
/// </summary>
|
||||
public const ushort MAX_DISCRETE_READ_COUNT = 0x07D0; // 2000
|
||||
|
||||
/// <summary>
|
||||
/// The maximum allowed write count for discrete values specified by the Modbus TCP protocol.
|
||||
/// </summary>
|
||||
public const ushort MAX_DISCRETE_WRITE_COUNT = 0x07B0; // 1968
|
||||
|
||||
/// <summary>
|
||||
/// The maximum allowed read count for registers specified by the Modbus TCP protocol.
|
||||
/// </summary>
|
||||
public const ushort MAX_REGISTER_READ_COUNT = 0x007D; // 125
|
||||
|
||||
/// <summary>
|
||||
/// The maximum allowed write count for registers specified by the Modbus TCP protocol.
|
||||
/// </summary>
|
||||
public const ushort MAX_REGISTER_WRITE_COUNT = 0x007B; // 123
|
||||
|
||||
#endregion Constants
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "TCP";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to disable the transaction id usage.
|
||||
/// </summary>
|
||||
public bool DisableTransactionId { get; set; }
|
||||
|
||||
#region Read
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<byte> SerializeReadCoils(byte unitId, ushort startAddress, ushort count)
|
||||
{
|
||||
// Technically not possible to reach. Left here for completeness.
|
||||
if (unitId < MIN_UNIT_ID || MAX_UNIT_ID < unitId)
|
||||
throw new ArgumentOutOfRangeException(nameof(unitId));
|
||||
|
||||
if (count < MIN_READ_COUNT || MAX_DISCRETE_READ_COUNT < count)
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
|
||||
if (ushort.MaxValue < (startAddress + count - 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(count), $"Combination of {nameof(startAddress)} and {nameof(count)} exceeds the addressation limit of {ushort.MaxValue}");
|
||||
|
||||
byte[] request = new byte[12];
|
||||
|
||||
byte[] header = GetHeader(unitId, 6);
|
||||
Array.Copy(header, 0, request, 0, header.Length);
|
||||
|
||||
// Function code
|
||||
request[7] = (byte)ModbusFunctionCode.ReadCoils;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = ToNetworkBytes(startAddress);
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = ToNetworkBytes(count);
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<Coil> DeserializeReadCoils(IReadOnlyList<byte> response)
|
||||
{
|
||||
int baseOffset = 9;
|
||||
if (response[8] != response.Count - baseOffset)
|
||||
throw new ModbusException("Coil byte count does not match.");
|
||||
|
||||
int count = response[8] * 8;
|
||||
var coils = new List<Coil>();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int bytePosition = i / 8;
|
||||
int bitPosition = i % 8;
|
||||
|
||||
int value = response[baseOffset + bytePosition] & (1 << bitPosition);
|
||||
coils.Add(new Coil
|
||||
{
|
||||
Address = (ushort)i,
|
||||
Value = value > 0
|
||||
});
|
||||
}
|
||||
|
||||
return coils;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<byte> SerializeReadDiscreteInputs(byte unitId, ushort startAddress, ushort count)
|
||||
{
|
||||
// Technically not possible to reach. Left here for completeness.
|
||||
if (unitId < MIN_UNIT_ID || MAX_UNIT_ID < unitId)
|
||||
throw new ArgumentOutOfRangeException(nameof(unitId));
|
||||
|
||||
if (count < MIN_READ_COUNT || MAX_DISCRETE_READ_COUNT < count)
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
|
||||
if (ushort.MaxValue < (startAddress + count - 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(count), $"Combination of {nameof(startAddress)} and {nameof(count)} exceeds the addressation limit of {ushort.MaxValue}");
|
||||
|
||||
byte[] request = new byte[12];
|
||||
|
||||
byte[] header = GetHeader(unitId, 6);
|
||||
Array.Copy(header, 0, request, 0, header.Length);
|
||||
|
||||
// Function code
|
||||
request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = ToNetworkBytes(startAddress);
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = ToNetworkBytes(count);
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<DiscreteInput> DeserializeReadDiscreteInputs(IReadOnlyList<byte> response)
|
||||
{
|
||||
int baseOffset = 9;
|
||||
if (response[8] != response.Count - baseOffset)
|
||||
throw new ModbusException("Discrete input byte count does not match.");
|
||||
|
||||
int count = response[8] * 8;
|
||||
var discreteInputs = new List<DiscreteInput>();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int bytePosition = i / 8;
|
||||
int bitPosition = i % 8;
|
||||
|
||||
int value = response[baseOffset + bytePosition] & (1 << bitPosition);
|
||||
discreteInputs.Add(new DiscreteInput
|
||||
{
|
||||
Address = (ushort)i,
|
||||
HighByte = (byte)(value > 0 ? 0xFF : 0x00)
|
||||
});
|
||||
}
|
||||
|
||||
return discreteInputs;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<byte> SerializeReadHoldingRegisters(byte unitId, ushort startAddress, ushort count)
|
||||
{
|
||||
// Technically not possible to reach. Left here for completeness.
|
||||
if (unitId < MIN_UNIT_ID || MAX_UNIT_ID < unitId)
|
||||
throw new ArgumentOutOfRangeException(nameof(unitId));
|
||||
|
||||
if (count < MIN_READ_COUNT || MAX_REGISTER_READ_COUNT < count)
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
|
||||
if (ushort.MaxValue < (startAddress + count - 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(count), $"Combination of {nameof(startAddress)} and {nameof(count)} exceeds the addressation limit of {ushort.MaxValue}");
|
||||
|
||||
byte[] request = new byte[12];
|
||||
|
||||
byte[] header = GetHeader(unitId, 6);
|
||||
Array.Copy(header, 0, request, 0, header.Length);
|
||||
|
||||
// Function code
|
||||
request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = ToNetworkBytes(startAddress);
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = ToNetworkBytes(count);
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<HoldingRegister> DeserializeReadHoldingRegisters(IReadOnlyList<byte> response)
|
||||
{
|
||||
int baseOffset = 9;
|
||||
if (response[8] != response.Count - baseOffset)
|
||||
throw new ModbusException("Holding register byte count does not match.");
|
||||
|
||||
int count = response[8] / 2;
|
||||
var holdingRegisters = new List<HoldingRegister>();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
holdingRegisters.Add(new HoldingRegister
|
||||
{
|
||||
Address = (ushort)i,
|
||||
HighByte = response[baseOffset + i * 2],
|
||||
LowByte = response[baseOffset + i * 2 + 1]
|
||||
});
|
||||
}
|
||||
|
||||
return holdingRegisters;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<byte> SerializeReadInputRegisters(byte unitId, ushort startAddress, ushort count)
|
||||
{
|
||||
// Technically not possible to reach. Left here for completeness.
|
||||
if (unitId < MIN_UNIT_ID || MAX_UNIT_ID < unitId)
|
||||
throw new ArgumentOutOfRangeException(nameof(unitId));
|
||||
|
||||
if (count < MIN_READ_COUNT || MAX_REGISTER_READ_COUNT < count)
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
|
||||
if (ushort.MaxValue < (startAddress + count - 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(count), $"Combination of {nameof(startAddress)} and {nameof(count)} exceeds the addressation limit of {ushort.MaxValue}");
|
||||
|
||||
byte[] request = new byte[12];
|
||||
|
||||
byte[] header = GetHeader(unitId, 6);
|
||||
Array.Copy(header, 0, request, 0, header.Length);
|
||||
|
||||
// Function code
|
||||
request[7] = (byte)ModbusFunctionCode.ReadInputRegisters;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = ToNetworkBytes(startAddress);
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = ToNetworkBytes(count);
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<InputRegister> DeserializeReadInputRegisters(IReadOnlyList<byte> response)
|
||||
{
|
||||
int baseOffset = 9;
|
||||
if (response[8] != response.Count - baseOffset)
|
||||
throw new ModbusException("Input register byte count does not match.");
|
||||
|
||||
int count = response[8] / 2;
|
||||
var inputRegisters = new List<InputRegister>();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
inputRegisters.Add(new InputRegister
|
||||
{
|
||||
Address = (ushort)i,
|
||||
HighByte = response[baseOffset + i * 2],
|
||||
LowByte = response[baseOffset + i * 2 + 1]
|
||||
});
|
||||
}
|
||||
|
||||
return inputRegisters;
|
||||
}
|
||||
|
||||
#endregion Read
|
||||
|
||||
#region Write
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<byte> SerializeWriteSingleCoil(byte unitId, Coil coil)
|
||||
{
|
||||
// Technically not possible to reach. Left here for completeness.
|
||||
if (unitId < MIN_UNIT_ID || MAX_UNIT_ID < unitId)
|
||||
throw new ArgumentOutOfRangeException(nameof(unitId));
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(coil);
|
||||
#else
|
||||
if (coil == null)
|
||||
throw new ArgumentNullException(nameof(coil));
|
||||
#endif
|
||||
|
||||
byte[] request = new byte[12];
|
||||
|
||||
byte[] header = GetHeader(unitId, 6);
|
||||
Array.Copy(header, 0, request, 0, header.Length);
|
||||
|
||||
// Function code
|
||||
request[7] = (byte)ModbusFunctionCode.WriteSingleCoil;
|
||||
|
||||
byte[] addrBytes = ToNetworkBytes(coil.Address);
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
request[10] = coil.HighByte;
|
||||
request[11] = coil.LowByte;
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Coil DeserializeWriteSingleCoil(IReadOnlyList<byte> response)
|
||||
{
|
||||
return new Coil
|
||||
{
|
||||
Address = ToNetworkUInt16(response.Skip(8).Take(2).ToArray()),
|
||||
HighByte = response[10],
|
||||
LowByte = response[11]
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<byte> SerializeWriteSingleHoldingRegister(byte unitId, HoldingRegister register)
|
||||
{
|
||||
// Technically not possible to reach. Left here for completeness.
|
||||
if (unitId < MIN_UNIT_ID || MAX_UNIT_ID < unitId)
|
||||
throw new ArgumentOutOfRangeException(nameof(unitId));
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(register);
|
||||
#else
|
||||
if (register == null)
|
||||
throw new ArgumentNullException(nameof(register));
|
||||
#endif
|
||||
|
||||
byte[] request = new byte[12];
|
||||
|
||||
byte[] header = GetHeader(unitId, 6);
|
||||
Array.Copy(header, 0, request, 0, header.Length);
|
||||
|
||||
// Function code
|
||||
request[7] = (byte)ModbusFunctionCode.WriteSingleRegister;
|
||||
|
||||
byte[] addrBytes = ToNetworkBytes(register.Address);
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
request[10] = register.HighByte;
|
||||
request[11] = register.LowByte;
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public HoldingRegister DeserializeWriteSingleHoldingRegister(IReadOnlyList<byte> response)
|
||||
{
|
||||
return new HoldingRegister
|
||||
{
|
||||
Address = ToNetworkUInt16(response.Skip(8).Take(2).ToArray()),
|
||||
HighByte = response[10],
|
||||
LowByte = response[11]
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<byte> SerializeWriteMultipleCoils(byte unitId, IReadOnlyList<Coil> coils)
|
||||
{
|
||||
// Technically not possible to reach. Left here for completeness.
|
||||
if (unitId < MIN_UNIT_ID || MAX_UNIT_ID < unitId)
|
||||
throw new ArgumentOutOfRangeException(nameof(unitId));
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(coils);
|
||||
#else
|
||||
if (coils == null)
|
||||
throw new ArgumentNullException(nameof(coils));
|
||||
#endif
|
||||
|
||||
var orderedList = coils.OrderBy(c => c.Address).ToList();
|
||||
if (orderedList.Count < MIN_WRITE_COUNT || MAX_DISCRETE_WRITE_COUNT < orderedList.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(coils), $"At least {MIN_WRITE_COUNT} or max. {MAX_DISCRETE_WRITE_COUNT} coils can be written at once.");
|
||||
|
||||
int addrCount = coils.Select(c => c.Address).Distinct().Count();
|
||||
if (orderedList.Count != addrCount)
|
||||
throw new ArgumentException("One or more duplicate coils found.", nameof(coils));
|
||||
|
||||
ushort firstAddress = orderedList.First().Address;
|
||||
ushort lastAddress = orderedList.Last().Address;
|
||||
|
||||
if (firstAddress + orderedList.Count - 1 != lastAddress)
|
||||
throw new ArgumentException("Gap in coil list found.", nameof(coils));
|
||||
|
||||
byte byteCount = (byte)Math.Ceiling(orderedList.Count / 8.0);
|
||||
byte[] request = new byte[13 + byteCount];
|
||||
|
||||
byte[] header = GetHeader(unitId, byteCount + 7);
|
||||
Array.Copy(header, 0, request, 0, header.Length);
|
||||
|
||||
request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils;
|
||||
|
||||
byte[] addrBytes = ToNetworkBytes(firstAddress);
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
byte[] countBytes = ToNetworkBytes((ushort)orderedList.Count);
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
request[12] = byteCount;
|
||||
|
||||
int baseOffset = 13;
|
||||
for (int i = 0; i < orderedList.Count; i++)
|
||||
{
|
||||
int bytePosition = i / 8;
|
||||
int bitPosition = i % 8;
|
||||
|
||||
if (orderedList[i].Value)
|
||||
{
|
||||
byte bitMask = (byte)(1 << bitPosition);
|
||||
request[baseOffset + bytePosition] |= bitMask;
|
||||
}
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public (ushort FirstAddress, ushort NumberOfCoils) DeserializeWriteMultipleCoils(IReadOnlyList<byte> response)
|
||||
{
|
||||
ushort firstAddress = ToNetworkUInt16(response.Skip(8).Take(2).ToArray());
|
||||
ushort numberOfCoils = ToNetworkUInt16(response.Skip(10).Take(2).ToArray());
|
||||
|
||||
return (firstAddress, numberOfCoils);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<byte> SerializeWriteMultipleHoldingRegisters(byte unitId, IReadOnlyList<HoldingRegister> registers)
|
||||
{
|
||||
// Technically not possible to reach. Left here for completeness.
|
||||
if (unitId < MIN_UNIT_ID || MAX_UNIT_ID < unitId)
|
||||
throw new ArgumentOutOfRangeException(nameof(unitId));
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(registers);
|
||||
#else
|
||||
if (registers == null)
|
||||
throw new ArgumentNullException(nameof(registers));
|
||||
#endif
|
||||
|
||||
var orderedList = registers.OrderBy(c => c.Address).ToList();
|
||||
if (orderedList.Count < MIN_WRITE_COUNT || MAX_REGISTER_WRITE_COUNT < orderedList.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(registers), $"At least {MIN_WRITE_COUNT} or max. {MAX_REGISTER_WRITE_COUNT} holding registers can be written at once.");
|
||||
|
||||
int addrCount = registers.Select(c => c.Address).Distinct().Count();
|
||||
if (orderedList.Count != addrCount)
|
||||
throw new ArgumentException("One or more duplicate holding registers found.", nameof(registers));
|
||||
|
||||
ushort firstAddress = orderedList.First().Address;
|
||||
ushort lastAddress = orderedList.Last().Address;
|
||||
|
||||
if (firstAddress + orderedList.Count - 1 != lastAddress)
|
||||
throw new ArgumentException("Gap in holding register list found.", nameof(registers));
|
||||
|
||||
byte byteCount = (byte)(orderedList.Count * 2);
|
||||
byte[] request = new byte[13 + byteCount];
|
||||
|
||||
byte[] header = GetHeader(unitId, byteCount + 7);
|
||||
Array.Copy(header, 0, request, 0, header.Length);
|
||||
|
||||
request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
|
||||
|
||||
byte[] addrBytes = ToNetworkBytes(firstAddress);
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
byte[] countBytes = ToNetworkBytes((ushort)orderedList.Count);
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
request[12] = byteCount;
|
||||
|
||||
int baseOffset = 13;
|
||||
for (int i = 0; i < orderedList.Count; i++)
|
||||
{
|
||||
request[baseOffset + 2 * i] = orderedList[i].HighByte;
|
||||
request[baseOffset + 2 * i + 1] = orderedList[i].LowByte;
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public (ushort FirstAddress, ushort NumberOfRegisters) DeserializeWriteMultipleHoldingRegisters(IReadOnlyList<byte> response)
|
||||
{
|
||||
ushort firstAddress = ToNetworkUInt16(response.Skip(8).Take(2).ToArray());
|
||||
ushort numberOfRegisters = ToNetworkUInt16(response.Skip(10).Take(2).ToArray());
|
||||
|
||||
return (firstAddress, numberOfRegisters);
|
||||
}
|
||||
|
||||
#endregion Write
|
||||
|
||||
#region Validation
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool CheckResponseComplete(IReadOnlyList<byte> responseBytes)
|
||||
{
|
||||
// 2x Transaction Id
|
||||
// 2x Protocol Identifier
|
||||
// 2x Number of following bytes
|
||||
if (responseBytes.Count < 6)
|
||||
return false;
|
||||
|
||||
ushort followingBytes = ToNetworkUInt16(responseBytes.Skip(4).Take(2).ToArray());
|
||||
if (responseBytes.Count < followingBytes + 6)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ValidateResponse(IReadOnlyList<byte> request, IReadOnlyList<byte> response)
|
||||
{
|
||||
if (!DisableTransactionId)
|
||||
{
|
||||
if (request[0] != response[0] || request[1] != response[1])
|
||||
throw new ModbusException("Transaction Id does not match.");
|
||||
}
|
||||
|
||||
if (request[2] != response[2] || request[3] != response[3])
|
||||
throw new ModbusException("Protocol Identifier does not match.");
|
||||
|
||||
ushort count = ToNetworkUInt16(response.Skip(4).Take(2).ToArray());
|
||||
if (count != response.Count - 6)
|
||||
throw new ModbusException("Number of following bytes does not match.");
|
||||
|
||||
if (request[6] != response[6])
|
||||
throw new ModbusException("Unit Identifier does not match.");
|
||||
|
||||
byte fnCode = response[7];
|
||||
bool isError = (fnCode & 0x80) == 0x80;
|
||||
if (isError)
|
||||
fnCode = (byte)(fnCode ^ 0x80); // === fnCode & 0x7F
|
||||
|
||||
if (request[7] != fnCode)
|
||||
throw new ModbusException("Function code does not match.");
|
||||
|
||||
if (isError)
|
||||
throw new ModbusException("Remote Error") { ErrorCode = (ModbusErrorCode)response[8] };
|
||||
}
|
||||
|
||||
#endregion Validation
|
||||
|
||||
#region Private helpers
|
||||
|
||||
private ushort GetNextTransacitonId()
|
||||
{
|
||||
if (DisableTransactionId)
|
||||
return 0x0000;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_transactionId == ushort.MaxValue)
|
||||
_transactionId = 0x0000;
|
||||
else
|
||||
_transactionId++;
|
||||
|
||||
return _transactionId;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] GetHeader(byte unitId, int followingBytes)
|
||||
{
|
||||
byte[] header = new byte[7];
|
||||
|
||||
// Transaction id
|
||||
ushort txId = GetNextTransacitonId();
|
||||
byte[] txBytes = ToNetworkBytes(txId);
|
||||
header[0] = txBytes[0];
|
||||
header[1] = txBytes[1];
|
||||
|
||||
// Protocol identifier
|
||||
header[2] = 0x00;
|
||||
header[3] = 0x00;
|
||||
|
||||
// Number of following bytes
|
||||
byte[] countBytes = ToNetworkBytes((ushort)followingBytes);
|
||||
header[4] = countBytes[0];
|
||||
header[5] = countBytes[1];
|
||||
|
||||
// Unit identifier
|
||||
header[6] = unitId;
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
private static byte[] ToNetworkBytes(ushort value)
|
||||
{
|
||||
byte[] bytes = BitConverter.GetBytes(value);
|
||||
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(bytes);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static ushort ToNetworkUInt16(byte[] bytes)
|
||||
{
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(bytes);
|
||||
|
||||
return BitConverter.ToUInt16(bytes, 0);
|
||||
}
|
||||
|
||||
#endregion Private helpers
|
||||
}
|
||||
}
|
||||
55
AMWD.Protocols.Modbus.Common/README.md
Normal file
55
AMWD.Protocols.Modbus.Common/README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Modbus Protocol for .NET | Common
|
||||
|
||||
This package contains all basic tools to build your own clients.
|
||||
|
||||
### Contracts
|
||||
|
||||
**IModbusConnection**
|
||||
This is the interface used on the base client to communicate with the remote device.
|
||||
If you want to use a custom connection type, you should implement this interface yourself.
|
||||
|
||||
**IModbusProtocol**
|
||||
If you want to speak a custom type of protocol with the clients, you can implement this interface.
|
||||
|
||||
**ModbusBaseClient**
|
||||
This abstract base client contains all the basic methods and handlings required to communicate via Modbus Protocol.
|
||||
The packages `AMWD.Protocols.Modbus.Serial` _(in progress)_ and `AMWD.Protocols.Modbus.Tcp` have specific derived implementations to match the communication types.
|
||||
|
||||
|
||||
### Enums
|
||||
|
||||
Here you have all typed enumerables defined by the Modbus Protocol.
|
||||
|
||||
|
||||
### Extensions
|
||||
|
||||
To convert the Modbus specific types to usable values and vice-versa, there are some extensions.
|
||||
|
||||
- Decimal extensions for `float` (single) and `double`
|
||||
- Signed extensions for signed integer values as `sbyte`, `short` (int16), `int` (int32) and `long` (int64)
|
||||
- Unsigned extensions for unsigned integer values as `byte`, `ushort` (uint16), `uint` (uint32) and `ulong` (uint64)
|
||||
- Some other extensions for `string` and `bool`
|
||||
|
||||
|
||||
### Models
|
||||
|
||||
The different types handled by the Modbus Protocol.
|
||||
|
||||
- Coil
|
||||
- Discrete Input
|
||||
- Holding Register
|
||||
- Input Register
|
||||
|
||||
|
||||
### Protocols
|
||||
|
||||
Here you have the specific default implementations for the Modbus Protocol.
|
||||
|
||||
- ASCII _(in progress)_
|
||||
- RTU _(in progress)_
|
||||
- TCP
|
||||
|
||||
|
||||
---
|
||||
|
||||
Published under MIT License (see [**tl;dr**Legal](https://www.tldrlegal.com/license/mit-license))
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
|
||||
<AssemblyName>amwd-modbus-serial</AssemblyName>
|
||||
<RootNamespace>AMWD.Protocols.Modbus.Serial</RootNamespace>
|
||||
|
||||
<Product>Modbus RTU/ASCII Protocol</Product>
|
||||
<Description>Implementation of the Modbus protocol communicating via serial line using RTU or ASCII encoding.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
40
AMWD.Protocols.Modbus.Serial/README.md
Normal file
40
AMWD.Protocols.Modbus.Serial/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Modbus Protocol for .NET | Serial
|
||||
|
||||
The Modbus Serial protocol implementation.
|
||||
|
||||
## Example
|
||||
|
||||
A simple example which reads the voltage between L1 and N of a Janitza device.
|
||||
|
||||
```csharp
|
||||
string serialPort = "COM5";
|
||||
|
||||
using var client = new ModbusSerialClient(serialPort);
|
||||
await client.ConnectAsync(CancellationToken.None);
|
||||
|
||||
byte unitId = 5;
|
||||
ushort startAddress = 19000;
|
||||
ushort count = 2;
|
||||
|
||||
var registers = await client.ReadHoldingRegistersAsync(unitId, startAddress, count);
|
||||
float voltage = registers.GetSingle();
|
||||
|
||||
Console.WriteLine($"The voltage between L1 and N is: {voltage:N2}V");
|
||||
```
|
||||
|
||||
|
||||
## Sources
|
||||
|
||||
- Protocol Specification: [v1.1b3]
|
||||
- Modbus Serial line: [v1.02]
|
||||
|
||||
|
||||
---
|
||||
|
||||
Published under MIT License (see [**tl;dr**Legal])
|
||||
|
||||
|
||||
|
||||
[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
|
||||
18
AMWD.Protocols.Modbus.Tcp/AMWD.Protocols.Modbus.Tcp.csproj
Normal file
18
AMWD.Protocols.Modbus.Tcp/AMWD.Protocols.Modbus.Tcp.csproj
Normal file
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
|
||||
<AssemblyName>amwd-modbus-tcp</AssemblyName>
|
||||
<RootNamespace>AMWD.Protocols.Modbus.Tcp</RootNamespace>
|
||||
|
||||
<Product>Modbus TCP Protocol</Product>
|
||||
<Description>Implementation of the Modbus protocol communicating via TCP.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
41
AMWD.Protocols.Modbus.Tcp/README.md
Normal file
41
AMWD.Protocols.Modbus.Tcp/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Modbus Protocol for .NET | TCP
|
||||
|
||||
The Modbus TCP protocol implementation.
|
||||
|
||||
## Example
|
||||
|
||||
A simple example which reads the voltage between L1 and N of a Janitza device.
|
||||
|
||||
```csharp
|
||||
string host = "modbus-device.internal";
|
||||
int port = 502;
|
||||
|
||||
using var client = new ModbusTcpClient(host, port);
|
||||
await client.ConnectAsync(CancellationToken.None);
|
||||
|
||||
byte unitId = 5;
|
||||
ushort startAddress = 19000;
|
||||
ushort count = 2;
|
||||
|
||||
var registers = await client.ReadHoldingRegistersAsync(unitId, startAddress, count);
|
||||
float voltage = registers.GetSingle();
|
||||
|
||||
Console.WriteLine($"The voltage between L1 and N is: {voltage:N2}V");
|
||||
```
|
||||
|
||||
|
||||
## Sources
|
||||
|
||||
- Protocol Specification: [v1.1b3]
|
||||
- Modbus TCP/IP: [v1.0b]
|
||||
|
||||
|
||||
---
|
||||
|
||||
Published under MIT License (see [**tl;dr**Legal])
|
||||
|
||||
|
||||
|
||||
[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
|
||||
@@ -0,0 +1,31 @@
|
||||
<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.msbuild" Version="6.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.2.0" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,742 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||
using Moq;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
|
||||
{
|
||||
[TestClass]
|
||||
public class ModbusClientBaseTest
|
||||
{
|
||||
// Consts
|
||||
private const byte UNIT_ID = 42;
|
||||
private const ushort START_ADDRESS = 123;
|
||||
private const ushort READ_COUNT = 12;
|
||||
|
||||
// Mocks
|
||||
private Mock<IModbusConnection> _connection;
|
||||
private Mock<IModbusProtocol> _protocol;
|
||||
|
||||
// Responses
|
||||
private bool _connectionIsConnectecd;
|
||||
private List<Coil> _readCoilsResponse;
|
||||
private List<DiscreteInput> _readDiscreteInputsResponse;
|
||||
private List<HoldingRegister> _readHoldingRegistersResponse;
|
||||
private List<InputRegister> _readInputRegistersResponse;
|
||||
private Coil _writeSingleCoilResponse;
|
||||
private HoldingRegister _writeSingleHoldingRegisterResponse;
|
||||
private (ushort startAddress, ushort count) _writeMultipleCoilsResponse;
|
||||
private (ushort startAddress, ushort count) _writeMultipleHoldingRegistersResponse;
|
||||
|
||||
[TestInitialize]
|
||||
public void Initialize()
|
||||
{
|
||||
_connectionIsConnectecd = true;
|
||||
|
||||
_readCoilsResponse = new List<Coil>();
|
||||
_readDiscreteInputsResponse = new List<DiscreteInput>();
|
||||
_readHoldingRegistersResponse = new List<HoldingRegister>();
|
||||
_readInputRegistersResponse = new List<InputRegister>();
|
||||
|
||||
for (int i = 0; i < READ_COUNT; i++)
|
||||
{
|
||||
_readCoilsResponse.Add(new Coil
|
||||
{
|
||||
Address = (ushort)i,
|
||||
HighByte = (byte)((i % 2 == 0) ? 0xFF : 0x00)
|
||||
});
|
||||
_readDiscreteInputsResponse.Add(new DiscreteInput
|
||||
{
|
||||
Address = (ushort)i,
|
||||
HighByte = (byte)((i % 2 == 1) ? 0xFF : 0x00)
|
||||
});
|
||||
|
||||
_readHoldingRegistersResponse.Add(new HoldingRegister
|
||||
{
|
||||
Address = (ushort)i,
|
||||
HighByte = 0x00,
|
||||
LowByte = (byte)(i + 10)
|
||||
});
|
||||
_readInputRegistersResponse.Add(new InputRegister
|
||||
{
|
||||
Address = (ushort)i,
|
||||
HighByte = 0x00,
|
||||
LowByte = (byte)(i + 15)
|
||||
});
|
||||
}
|
||||
|
||||
_writeSingleCoilResponse = new Coil { Address = START_ADDRESS };
|
||||
_writeSingleHoldingRegisterResponse = new HoldingRegister { Address = START_ADDRESS, Value = 0x1234 };
|
||||
|
||||
_writeMultipleCoilsResponse = (START_ADDRESS, READ_COUNT);
|
||||
_writeMultipleHoldingRegistersResponse = (START_ADDRESS, READ_COUNT);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldPrettyPrint()
|
||||
{
|
||||
// Arrange
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
string str = client.ToString();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("Modbus client using Moq protocol to connect via Mock", str);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowExceptionOnNullConnection()
|
||||
{
|
||||
// Arrange
|
||||
IModbusConnection connection = null;
|
||||
|
||||
// Act
|
||||
new ModbusClientBaseWrapper(connection);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldConnectSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
await client.ConnectAsync();
|
||||
|
||||
// Assert
|
||||
_connection.Verify(c => c.ConnectAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||
_connection.VerifyNoOtherCalls();
|
||||
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldDisconnectSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
await client.DisconnectAsync();
|
||||
|
||||
// Assert
|
||||
_connection.Verify(c => c.DisconnectAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||
_connection.VerifyNoOtherCalls();
|
||||
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(true)]
|
||||
[DataRow(false)]
|
||||
public void ShouldAlsoDisposeConnection(bool disposeConnection)
|
||||
{
|
||||
// Arrange
|
||||
var client = GetClient(disposeConnection);
|
||||
|
||||
// Act
|
||||
client.Dispose();
|
||||
|
||||
// Assert
|
||||
if (disposeConnection)
|
||||
_connection.Verify(c => c.Dispose(), Times.Once);
|
||||
|
||||
_connection.VerifyNoOtherCalls();
|
||||
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldAllowDisposeMultipleTimes()
|
||||
{
|
||||
// Arrange
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
client.Dispose();
|
||||
client.Dispose();
|
||||
|
||||
// Assert
|
||||
_connection.Verify(c => c.Dispose(), Times.Once);
|
||||
_connection.VerifyNoOtherCalls();
|
||||
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[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
|
||||
}
|
||||
|
||||
[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
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ApplicationException))]
|
||||
public async Task ShouldAssertConnected()
|
||||
{
|
||||
// Arrange
|
||||
_connectionIsConnectecd = false;
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
await client.ReadCoilsAsync(UNIT_ID, START_ADDRESS, READ_COUNT);
|
||||
|
||||
// Assert - ApplicationException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldReadCoils()
|
||||
{
|
||||
// Arrange
|
||||
_readCoilsResponse.Add(new Coil());
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
var result = await client.ReadCoilsAsync(UNIT_ID, START_ADDRESS, READ_COUNT);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(READ_COUNT, result.Count);
|
||||
|
||||
for (int i = 0; i < READ_COUNT; i++)
|
||||
{
|
||||
Assert.AreEqual(START_ADDRESS + i, result[i].Address);
|
||||
Assert.AreEqual(i % 2 == 0, result[i].Value);
|
||||
}
|
||||
|
||||
_connection.VerifyGet(c => c.IsConnected, Times.Once);
|
||||
_connection.Verify(c => c.InvokeAsync(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<Func<IReadOnlyList<byte>, bool>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_connection.VerifyNoOtherCalls();
|
||||
|
||||
_protocol.Verify(p => p.SerializeReadCoils(UNIT_ID, START_ADDRESS, READ_COUNT), Times.Once);
|
||||
_protocol.Verify(p => p.ValidateResponse(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.Verify(p => p.DeserializeReadCoils(It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldReadDiscreteInputs()
|
||||
{
|
||||
// Arrange
|
||||
_readDiscreteInputsResponse.Add(new DiscreteInput());
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
var result = await client.ReadDiscreteInputsAsync(UNIT_ID, START_ADDRESS, READ_COUNT);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(READ_COUNT, result.Count);
|
||||
|
||||
for (int i = 0; i < READ_COUNT; i++)
|
||||
{
|
||||
Assert.AreEqual(START_ADDRESS + i, result[i].Address);
|
||||
Assert.AreEqual(i % 2 == 1, result[i].Value);
|
||||
}
|
||||
|
||||
_connection.VerifyGet(c => c.IsConnected, Times.Once);
|
||||
_connection.Verify(c => c.InvokeAsync(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<Func<IReadOnlyList<byte>, bool>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_connection.VerifyNoOtherCalls();
|
||||
|
||||
_protocol.Verify(p => p.SerializeReadDiscreteInputs(UNIT_ID, START_ADDRESS, READ_COUNT), Times.Once);
|
||||
_protocol.Verify(p => p.ValidateResponse(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.Verify(p => p.DeserializeReadDiscreteInputs(It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldReadHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
var result = await client.ReadHoldingRegistersAsync(UNIT_ID, START_ADDRESS, READ_COUNT);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(READ_COUNT, result.Count);
|
||||
|
||||
for (int i = 0; i < READ_COUNT; i++)
|
||||
{
|
||||
Assert.AreEqual(START_ADDRESS + i, result[i].Address);
|
||||
Assert.AreEqual(i + 10, result[i].Value);
|
||||
}
|
||||
|
||||
_connection.VerifyGet(c => c.IsConnected, Times.Once);
|
||||
_connection.Verify(c => c.InvokeAsync(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<Func<IReadOnlyList<byte>, bool>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_connection.VerifyNoOtherCalls();
|
||||
|
||||
_protocol.Verify(p => p.SerializeReadHoldingRegisters(UNIT_ID, START_ADDRESS, READ_COUNT), Times.Once);
|
||||
_protocol.Verify(p => p.ValidateResponse(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.Verify(p => p.DeserializeReadHoldingRegisters(It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldReadInputRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
var result = await client.ReadInputRegistersAsync(UNIT_ID, START_ADDRESS, READ_COUNT);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(READ_COUNT, result.Count);
|
||||
|
||||
for (int i = 0; i < READ_COUNT; i++)
|
||||
{
|
||||
Assert.AreEqual(START_ADDRESS + i, result[i].Address);
|
||||
Assert.AreEqual(i + 15, result[i].Value);
|
||||
}
|
||||
|
||||
_connection.VerifyGet(c => c.IsConnected, Times.Once);
|
||||
_connection.Verify(c => c.InvokeAsync(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<Func<IReadOnlyList<byte>, bool>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_connection.VerifyNoOtherCalls();
|
||||
|
||||
_protocol.Verify(p => p.SerializeReadInputRegisters(UNIT_ID, START_ADDRESS, READ_COUNT), Times.Once);
|
||||
_protocol.Verify(p => p.ValidateResponse(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.Verify(p => p.DeserializeReadInputRegisters(It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldWriteSingleCoil()
|
||||
{
|
||||
// Arrange
|
||||
var coil = new Coil
|
||||
{
|
||||
Address = START_ADDRESS,
|
||||
Value = false
|
||||
};
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
bool result = await client.WriteSingleCoilAsync(UNIT_ID, coil);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
|
||||
_connection.VerifyGet(c => c.IsConnected, Times.Once);
|
||||
_connection.Verify(c => c.InvokeAsync(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<Func<IReadOnlyList<byte>, bool>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_connection.VerifyNoOtherCalls();
|
||||
|
||||
_protocol.Verify(p => p.SerializeWriteSingleCoil(UNIT_ID, coil), Times.Once);
|
||||
_protocol.Verify(p => p.ValidateResponse(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.Verify(p => p.DeserializeWriteSingleCoil(It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldFailWriteSingleCoilOnAddress()
|
||||
{
|
||||
// Arrange
|
||||
var coil = new Coil
|
||||
{
|
||||
Address = START_ADDRESS + 1,
|
||||
Value = false
|
||||
};
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
bool result = await client.WriteSingleCoilAsync(UNIT_ID, coil);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
|
||||
_connection.VerifyGet(c => c.IsConnected, Times.Once);
|
||||
_connection.Verify(c => c.InvokeAsync(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<Func<IReadOnlyList<byte>, bool>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_connection.VerifyNoOtherCalls();
|
||||
|
||||
_protocol.Verify(p => p.SerializeWriteSingleCoil(UNIT_ID, coil), Times.Once);
|
||||
_protocol.Verify(p => p.ValidateResponse(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.Verify(p => p.DeserializeWriteSingleCoil(It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldFailWriteSingleCoilOnValue()
|
||||
{
|
||||
// Arrange
|
||||
var coil = new Coil
|
||||
{
|
||||
Address = START_ADDRESS,
|
||||
Value = true
|
||||
};
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
bool result = await client.WriteSingleCoilAsync(UNIT_ID, coil);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
|
||||
_connection.VerifyGet(c => c.IsConnected, Times.Once);
|
||||
_connection.Verify(c => c.InvokeAsync(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<Func<IReadOnlyList<byte>, bool>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_connection.VerifyNoOtherCalls();
|
||||
|
||||
_protocol.Verify(p => p.SerializeWriteSingleCoil(UNIT_ID, coil), Times.Once);
|
||||
_protocol.Verify(p => p.ValidateResponse(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.Verify(p => p.DeserializeWriteSingleCoil(It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldWriteSingleHoldingRegister()
|
||||
{
|
||||
// Arrange
|
||||
var register = new HoldingRegister
|
||||
{
|
||||
Address = START_ADDRESS,
|
||||
Value = 0x1234
|
||||
};
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
bool result = await client.WriteSingleHoldingRegisterAsync(UNIT_ID, register);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
|
||||
_connection.VerifyGet(c => c.IsConnected, Times.Once);
|
||||
_connection.Verify(c => c.InvokeAsync(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<Func<IReadOnlyList<byte>, bool>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_connection.VerifyNoOtherCalls();
|
||||
|
||||
_protocol.Verify(p => p.SerializeWriteSingleHoldingRegister(UNIT_ID, register), Times.Once);
|
||||
_protocol.Verify(p => p.ValidateResponse(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.Verify(p => p.DeserializeWriteSingleHoldingRegister(It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldFailWriteSingleHoldingRegisterOnAddress()
|
||||
{
|
||||
// Arrange
|
||||
var register = new HoldingRegister
|
||||
{
|
||||
Address = START_ADDRESS + 1,
|
||||
Value = 0x1234
|
||||
};
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
bool result = await client.WriteSingleHoldingRegisterAsync(UNIT_ID, register);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
|
||||
_connection.VerifyGet(c => c.IsConnected, Times.Once);
|
||||
_connection.Verify(c => c.InvokeAsync(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<Func<IReadOnlyList<byte>, bool>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_connection.VerifyNoOtherCalls();
|
||||
|
||||
_protocol.Verify(p => p.SerializeWriteSingleHoldingRegister(UNIT_ID, register), Times.Once);
|
||||
_protocol.Verify(p => p.ValidateResponse(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.Verify(p => p.DeserializeWriteSingleHoldingRegister(It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldFailWriteSingleHoldingRegisterOnValue()
|
||||
{
|
||||
// Arrange
|
||||
var register = new HoldingRegister
|
||||
{
|
||||
Address = START_ADDRESS,
|
||||
Value = 0x1233
|
||||
};
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
bool result = await client.WriteSingleHoldingRegisterAsync(UNIT_ID, register);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
|
||||
_connection.VerifyGet(c => c.IsConnected, Times.Once);
|
||||
_connection.Verify(c => c.InvokeAsync(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<Func<IReadOnlyList<byte>, bool>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_connection.VerifyNoOtherCalls();
|
||||
|
||||
_protocol.Verify(p => p.SerializeWriteSingleHoldingRegister(UNIT_ID, register), Times.Once);
|
||||
_protocol.Verify(p => p.ValidateResponse(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.Verify(p => p.DeserializeWriteSingleHoldingRegister(It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldWriteMultipleCoils()
|
||||
{
|
||||
// Arrange
|
||||
var coils = new List<Coil>();
|
||||
for (int i = 0; i < READ_COUNT; i++)
|
||||
{
|
||||
coils.Add(new Coil
|
||||
{
|
||||
Address = (ushort)(START_ADDRESS + i),
|
||||
Value = i % 2 == 0
|
||||
});
|
||||
}
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
bool result = await client.WriteMultipleCoilsAsync(UNIT_ID, coils);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
|
||||
_connection.VerifyGet(c => c.IsConnected, Times.Once);
|
||||
_connection.Verify(c => c.InvokeAsync(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<Func<IReadOnlyList<byte>, bool>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_connection.VerifyNoOtherCalls();
|
||||
|
||||
_protocol.Verify(p => p.SerializeWriteMultipleCoils(UNIT_ID, coils), Times.Once);
|
||||
_protocol.Verify(p => p.ValidateResponse(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.Verify(p => p.DeserializeWriteMultipleCoils(It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldFailWriteMultipleCoilsOnAddress()
|
||||
{
|
||||
// Arrange
|
||||
_writeMultipleCoilsResponse.startAddress = START_ADDRESS + 1;
|
||||
var coils = new List<Coil>();
|
||||
for (int i = 0; i < READ_COUNT; i++)
|
||||
{
|
||||
coils.Add(new Coil
|
||||
{
|
||||
Address = (ushort)(START_ADDRESS + i),
|
||||
Value = i % 2 == 0
|
||||
});
|
||||
}
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
bool result = await client.WriteMultipleCoilsAsync(UNIT_ID, coils);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
|
||||
_connection.VerifyGet(c => c.IsConnected, Times.Once);
|
||||
_connection.Verify(c => c.InvokeAsync(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<Func<IReadOnlyList<byte>, bool>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_connection.VerifyNoOtherCalls();
|
||||
|
||||
_protocol.Verify(p => p.SerializeWriteMultipleCoils(UNIT_ID, coils), Times.Once);
|
||||
_protocol.Verify(p => p.ValidateResponse(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.Verify(p => p.DeserializeWriteMultipleCoils(It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldFailWriteMultipleCoilsOnCount()
|
||||
{
|
||||
// Arrange
|
||||
_writeMultipleCoilsResponse.count = READ_COUNT + 1;
|
||||
var coils = new List<Coil>();
|
||||
for (int i = 0; i < READ_COUNT; i++)
|
||||
{
|
||||
coils.Add(new Coil
|
||||
{
|
||||
Address = (ushort)(START_ADDRESS + i),
|
||||
Value = i % 2 == 0
|
||||
});
|
||||
}
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
bool result = await client.WriteMultipleCoilsAsync(UNIT_ID, coils);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
|
||||
_connection.VerifyGet(c => c.IsConnected, Times.Once);
|
||||
_connection.Verify(c => c.InvokeAsync(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<Func<IReadOnlyList<byte>, bool>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_connection.VerifyNoOtherCalls();
|
||||
|
||||
_protocol.Verify(p => p.SerializeWriteMultipleCoils(UNIT_ID, coils), Times.Once);
|
||||
_protocol.Verify(p => p.ValidateResponse(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.Verify(p => p.DeserializeWriteMultipleCoils(It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldWriteMultipleRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new List<HoldingRegister>();
|
||||
for (int i = 0; i < READ_COUNT; i++)
|
||||
{
|
||||
registers.Add(new HoldingRegister
|
||||
{
|
||||
Address = (ushort)(START_ADDRESS + i),
|
||||
Value = (ushort)i
|
||||
});
|
||||
}
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
bool result = await client.WriteMultipleHoldingRegistersAsync(UNIT_ID, registers);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
|
||||
_connection.VerifyGet(c => c.IsConnected, Times.Once);
|
||||
_connection.Verify(c => c.InvokeAsync(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<Func<IReadOnlyList<byte>, bool>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_connection.VerifyNoOtherCalls();
|
||||
|
||||
_protocol.Verify(p => p.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers), Times.Once);
|
||||
_protocol.Verify(p => p.ValidateResponse(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.Verify(p => p.DeserializeWriteMultipleHoldingRegisters(It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldFailWriteMultiplRegistersOnAddress()
|
||||
{
|
||||
// Arrange
|
||||
_writeMultipleHoldingRegistersResponse.startAddress = START_ADDRESS + 1;
|
||||
var registers = new List<HoldingRegister>();
|
||||
for (int i = 0; i < READ_COUNT; i++)
|
||||
{
|
||||
registers.Add(new HoldingRegister
|
||||
{
|
||||
Address = (ushort)(START_ADDRESS + i),
|
||||
Value = (ushort)i
|
||||
});
|
||||
}
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
bool result = await client.WriteMultipleHoldingRegistersAsync(UNIT_ID, registers);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
|
||||
_connection.VerifyGet(c => c.IsConnected, Times.Once);
|
||||
_connection.Verify(c => c.InvokeAsync(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<Func<IReadOnlyList<byte>, bool>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_connection.VerifyNoOtherCalls();
|
||||
|
||||
_protocol.Verify(p => p.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers), Times.Once);
|
||||
_protocol.Verify(p => p.ValidateResponse(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.Verify(p => p.DeserializeWriteMultipleHoldingRegisters(It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldFailWriteMultipleRegistersOnCount()
|
||||
{
|
||||
// Arrange
|
||||
_writeMultipleHoldingRegistersResponse.count = READ_COUNT + 1;
|
||||
var registers = new List<HoldingRegister>();
|
||||
for (int i = 0; i < READ_COUNT; i++)
|
||||
{
|
||||
registers.Add(new HoldingRegister
|
||||
{
|
||||
Address = (ushort)(START_ADDRESS + i),
|
||||
Value = (ushort)i
|
||||
});
|
||||
}
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
bool result = await client.WriteMultipleHoldingRegistersAsync(UNIT_ID, registers);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
|
||||
_connection.VerifyGet(c => c.IsConnected, Times.Once);
|
||||
_connection.Verify(c => c.InvokeAsync(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<Func<IReadOnlyList<byte>, bool>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_connection.VerifyNoOtherCalls();
|
||||
|
||||
_protocol.Verify(p => p.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers), Times.Once);
|
||||
_protocol.Verify(p => p.ValidateResponse(It.IsAny<IReadOnlyList<byte>>(), It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.Verify(p => p.DeserializeWriteMultipleHoldingRegisters(It.IsAny<IReadOnlyList<byte>>()), Times.Once);
|
||||
_protocol.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
private ModbusClientBase GetClient(bool disposeConnection = true)
|
||||
{
|
||||
_connection = new Mock<IModbusConnection>();
|
||||
_connection
|
||||
.SetupGet(c => c.Name)
|
||||
.Returns("Mock");
|
||||
_connection
|
||||
.SetupGet(c => c.IsConnected)
|
||||
.Returns(() => _connectionIsConnectecd);
|
||||
|
||||
_protocol = new Mock<IModbusProtocol>();
|
||||
_protocol
|
||||
.SetupGet(p => p.Name)
|
||||
.Returns("Moq");
|
||||
_protocol
|
||||
.Setup(p => p.DeserializeReadCoils(It.IsAny<IReadOnlyList<byte>>()))
|
||||
.Returns(() => _readCoilsResponse);
|
||||
_protocol
|
||||
.Setup(p => p.DeserializeReadDiscreteInputs(It.IsAny<IReadOnlyList<byte>>()))
|
||||
.Returns(() => _readDiscreteInputsResponse);
|
||||
_protocol
|
||||
.Setup(p => p.DeserializeReadHoldingRegisters(It.IsAny<IReadOnlyList<byte>>()))
|
||||
.Returns(() => _readHoldingRegistersResponse);
|
||||
_protocol
|
||||
.Setup(p => p.DeserializeReadInputRegisters(It.IsAny<IReadOnlyList<byte>>()))
|
||||
.Returns(() => _readInputRegistersResponse);
|
||||
|
||||
_protocol
|
||||
.Setup(p => p.DeserializeWriteSingleCoil(It.IsAny<IReadOnlyList<byte>>()))
|
||||
.Returns(() => _writeSingleCoilResponse);
|
||||
_protocol
|
||||
.Setup(p => p.DeserializeWriteSingleHoldingRegister(It.IsAny<IReadOnlyList<byte>>()))
|
||||
.Returns(() => _writeSingleHoldingRegisterResponse);
|
||||
_protocol
|
||||
.Setup(p => p.DeserializeWriteMultipleCoils(It.IsAny<IReadOnlyList<byte>>()))
|
||||
.Returns(() => _writeMultipleCoilsResponse);
|
||||
_protocol
|
||||
.Setup(p => p.DeserializeWriteMultipleHoldingRegisters(It.IsAny<IReadOnlyList<byte>>()))
|
||||
.Returns(() => _writeMultipleHoldingRegistersResponse);
|
||||
|
||||
return new ModbusClientBaseWrapper(_connection.Object, disposeConnection)
|
||||
{
|
||||
Protocol = _protocol.Object,
|
||||
};
|
||||
}
|
||||
|
||||
internal class ModbusClientBaseWrapper : ModbusClientBase
|
||||
{
|
||||
public ModbusClientBaseWrapper(IModbusConnection connection)
|
||||
: base(connection)
|
||||
{ }
|
||||
|
||||
public ModbusClientBaseWrapper(IModbusConnection connection, bool disposeConnection)
|
||||
: base(connection, disposeConnection)
|
||||
{ }
|
||||
|
||||
public override IModbusProtocol Protocol { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
using AMWD.Protocols.Modbus.Common;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
|
||||
{
|
||||
[TestClass]
|
||||
public class ModbusDecimalExtensionsTest
|
||||
{
|
||||
#region Modbus to value
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetSingle()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new(),
|
||||
new() { Address = 100, HighByte = 0x41, LowByte = 0x45 },
|
||||
new() { Address = 101, HighByte = 0x70, LowByte = 0xA4 }
|
||||
};
|
||||
|
||||
// Act
|
||||
float f = registers.GetSingle(1);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(12.34f, f);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetSingleReversedRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new() { Address = 101, HighByte = 0x41, LowByte = 0x45 },
|
||||
new() { Address = 100, HighByte = 0x70, LowByte = 0xA4 }
|
||||
};
|
||||
|
||||
// Act
|
||||
float f = registers.GetSingle(0, reverseRegisterOrder: true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(12.34f, f);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullOnGetSingle()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister[] registers = null;
|
||||
|
||||
// Act
|
||||
registers.GetSingle(0);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetSingleForLength()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new() { Address = 101, HighByte = 0x01, LowByte = 0x02 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetSingle(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(1)]
|
||||
[DataRow(-1)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowArgumentOutOfRangeOnGetSingle(int startIndex)
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new() { Address = 101, HighByte = 0x01, LowByte = 0x02 },
|
||||
new() { Address = 100, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetSingle(startIndex);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetSingleForType()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new ModbusObject[]
|
||||
{
|
||||
new HoldingRegister { Address = 100, HighByte = 0x01, LowByte = 0x02 },
|
||||
new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetSingle(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetDouble()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new(),
|
||||
new() { Address = 100, HighByte = 0x40, LowByte = 0x28 },
|
||||
new() { Address = 101, HighByte = 0xAE, LowByte = 0x14 },
|
||||
new() { Address = 102, HighByte = 0x7A, LowByte = 0xE1 },
|
||||
new() { Address = 103, HighByte = 0x47, LowByte = 0xAE }
|
||||
};
|
||||
|
||||
// Act
|
||||
double d = registers.GetDouble(1);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(12.34, d);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetDoubleReversedRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new() { Address = 103, HighByte = 0x40, LowByte = 0x28 },
|
||||
new() { Address = 102, HighByte = 0xAE, LowByte = 0x14 },
|
||||
new() { Address = 101, HighByte = 0x7A, LowByte = 0xE1 },
|
||||
new() { Address = 100, HighByte = 0x47, LowByte = 0xAE }
|
||||
};
|
||||
|
||||
// Act
|
||||
double d = registers.GetDouble(0, reverseRegisterOrder: true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(12.34, d);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullOnGetDouble()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister[] registers = null;
|
||||
|
||||
// Act
|
||||
registers.GetDouble(0);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetDoubleForLength()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new() { Address = 100, HighByte = 0x40, LowByte = 0x28 },
|
||||
new() { Address = 101, HighByte = 0xAE, LowByte = 0x14 },
|
||||
new() { Address = 102, HighByte = 0x7A, LowByte = 0xE1 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetDouble(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(1)]
|
||||
[DataRow(-1)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowArgumentOutOfRangeOnGetDouble(int startIndex)
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new() { Address = 100, HighByte = 0x40, LowByte = 0x28 },
|
||||
new() { Address = 101, HighByte = 0xAE, LowByte = 0x14 },
|
||||
new() { Address = 102, HighByte = 0x7A, LowByte = 0xE1 },
|
||||
new() { Address = 103, HighByte = 0x47, LowByte = 0xAE }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetDouble(startIndex);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetDoubleForType()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new ModbusObject[]
|
||||
{
|
||||
new HoldingRegister { Address = 100, HighByte = 0x40, LowByte = 0x28 },
|
||||
new InputRegister { Address = 101, HighByte = 0xAE, LowByte = 0x14 },
|
||||
new HoldingRegister { Address = 102, HighByte = 0x7A, LowByte = 0xE1 },
|
||||
new InputRegister { Address = 103, HighByte = 0x47, LowByte = 0xAE }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetDouble(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
}
|
||||
|
||||
#endregion Modbus to value
|
||||
|
||||
#region Value to Modbus
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldConvertSingle()
|
||||
{
|
||||
// Arrange
|
||||
float f = 12.34f;
|
||||
|
||||
// Act
|
||||
var registers = f.ToRegister(5).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(registers);
|
||||
Assert.AreEqual(2, registers.Count);
|
||||
|
||||
Assert.AreEqual(5, registers[0].Address);
|
||||
Assert.AreEqual(0x41, registers[0].HighByte);
|
||||
Assert.AreEqual(0x45, registers[0].LowByte);
|
||||
|
||||
Assert.AreEqual(6, registers[1].Address);
|
||||
Assert.AreEqual(0x70, registers[1].HighByte);
|
||||
Assert.AreEqual(0xA4, registers[1].LowByte);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldConvertSingleReversed()
|
||||
{
|
||||
// Arrange
|
||||
float f = 12.34f;
|
||||
|
||||
// Act
|
||||
var registers = f.ToRegister(5, reverseRegisterOrder: true).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(registers);
|
||||
Assert.AreEqual(2, registers.Count);
|
||||
|
||||
Assert.AreEqual(6, registers[0].Address);
|
||||
Assert.AreEqual(0x41, registers[0].HighByte);
|
||||
Assert.AreEqual(0x45, registers[0].LowByte);
|
||||
|
||||
Assert.AreEqual(5, registers[1].Address);
|
||||
Assert.AreEqual(0x70, registers[1].HighByte);
|
||||
Assert.AreEqual(0xA4, registers[1].LowByte);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldConvertDouble()
|
||||
{
|
||||
// Arrange
|
||||
double d = 12.34;
|
||||
|
||||
// Act
|
||||
var registers = d.ToRegister(5).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(registers);
|
||||
Assert.AreEqual(4, registers.Count);
|
||||
|
||||
Assert.AreEqual(5, registers[0].Address);
|
||||
Assert.AreEqual(0x40, registers[0].HighByte);
|
||||
Assert.AreEqual(0x28, registers[0].LowByte);
|
||||
|
||||
Assert.AreEqual(6, registers[1].Address);
|
||||
Assert.AreEqual(0xAE, registers[1].HighByte);
|
||||
Assert.AreEqual(0x14, registers[1].LowByte);
|
||||
|
||||
Assert.AreEqual(7, registers[2].Address);
|
||||
Assert.AreEqual(0x7A, registers[2].HighByte);
|
||||
Assert.AreEqual(0xE1, registers[2].LowByte);
|
||||
|
||||
Assert.AreEqual(8, registers[3].Address);
|
||||
Assert.AreEqual(0x47, registers[3].HighByte);
|
||||
Assert.AreEqual(0xAE, registers[3].LowByte);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldConvertDoubleReversed()
|
||||
{
|
||||
// Arrange
|
||||
double d = 12.34;
|
||||
|
||||
// Act
|
||||
var registers = d.ToRegister(5, reverseRegisterOrder: true).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(registers);
|
||||
Assert.AreEqual(4, registers.Count);
|
||||
|
||||
Assert.AreEqual(8, registers[0].Address);
|
||||
Assert.AreEqual(0x40, registers[0].HighByte);
|
||||
Assert.AreEqual(0x28, registers[0].LowByte);
|
||||
|
||||
Assert.AreEqual(7, registers[1].Address);
|
||||
Assert.AreEqual(0xAE, registers[1].HighByte);
|
||||
Assert.AreEqual(0x14, registers[1].LowByte);
|
||||
|
||||
Assert.AreEqual(6, registers[2].Address);
|
||||
Assert.AreEqual(0x7A, registers[2].HighByte);
|
||||
Assert.AreEqual(0xE1, registers[2].LowByte);
|
||||
|
||||
Assert.AreEqual(5, registers[3].Address);
|
||||
Assert.AreEqual(0x47, registers[3].HighByte);
|
||||
Assert.AreEqual(0xAE, registers[3].LowByte);
|
||||
}
|
||||
|
||||
#endregion Value to Modbus
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
using System.Text;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
|
||||
{
|
||||
[TestClass]
|
||||
public class ModbusExtensionsTest
|
||||
{
|
||||
#region Modbus to value
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldConvertToBoolean()
|
||||
{
|
||||
// Arrange
|
||||
var coil = new Coil { HighByte = 0x00 };
|
||||
var discreteInput = new DiscreteInput { HighByte = 0xFF };
|
||||
var holdingRegister = new HoldingRegister { HighByte = 0x01 };
|
||||
var inputRegister = new InputRegister { LowByte = 0x10 };
|
||||
|
||||
// Act
|
||||
bool coilResult = coil.GetBoolean();
|
||||
bool discreteInputResult = discreteInput.GetBoolean();
|
||||
bool holdingRegisterResult = holdingRegister.GetBoolean();
|
||||
bool inputRegisterResult = inputRegister.GetBoolean();
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(coilResult);
|
||||
Assert.IsTrue(discreteInputResult);
|
||||
Assert.IsTrue(holdingRegisterResult);
|
||||
Assert.IsTrue(inputRegisterResult);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullOnGetBoolean()
|
||||
{
|
||||
// Arrange
|
||||
Coil coil = null;
|
||||
|
||||
// Act
|
||||
coil.GetBoolean();
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldConvertToString()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new() { Address = 1, HighByte = 65, LowByte = 66 },
|
||||
new() { Address = 2, HighByte = 67, LowByte = 0 },
|
||||
new() { Address = 3, HighByte = 95, LowByte = 96 }
|
||||
};
|
||||
|
||||
// Act
|
||||
string text = registers.GetString(3);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("ABC", text);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldConvertToStringReversedBytes()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new() { Address = 1, HighByte = 66, LowByte = 65 },
|
||||
new() { Address = 2, HighByte = 0, LowByte = 67 }
|
||||
};
|
||||
|
||||
// Act
|
||||
string text = registers.GetString(2, reverseByteOrderPerRegister: true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("ABC", text);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldConvertToStringReversedRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new() { Address = 2, HighByte = 65, LowByte = 66 },
|
||||
new() { Address = 1, HighByte = 67, LowByte = 0 },
|
||||
};
|
||||
|
||||
// Act
|
||||
string text = registers.GetString(2, reverseRegisterOrder: true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("ABC", text);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullOnString()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister[] list = null;
|
||||
|
||||
// Act
|
||||
list.GetString(2);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnStringForEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var registers = Array.Empty<HoldingRegister>();
|
||||
|
||||
// Act
|
||||
registers.GetString(2);
|
||||
|
||||
// Assert - ArgumentException
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(1)]
|
||||
[DataRow(-1)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowArgumentOutOfRangeOnString(int startIndex)
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new() { Address = 1, HighByte = 65, LowByte = 66 },
|
||||
new() { Address = 2, HighByte = 67, LowByte = 0 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetString(2, startIndex);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnStringForMixedTypes()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new ModbusObject[]
|
||||
{
|
||||
new HoldingRegister { Address = 1, HighByte = 65, LowByte = 66 },
|
||||
new InputRegister { Address = 2, HighByte = 67, LowByte = 0 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetString(2);
|
||||
|
||||
// Assert - ArgumentException
|
||||
}
|
||||
|
||||
#endregion Modbus to value
|
||||
|
||||
#region Value to Modbus
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetBooleanCoil()
|
||||
{
|
||||
// Arrange
|
||||
bool value = false;
|
||||
|
||||
// Act
|
||||
var coil = value.ToCoil(123);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(coil);
|
||||
Assert.AreEqual(123, coil.Address);
|
||||
Assert.IsFalse(coil.Value);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetBooleanRegisterTrue()
|
||||
{
|
||||
// Arrange
|
||||
bool value = true;
|
||||
|
||||
// Act
|
||||
var register = value.ToRegister(321);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(register);
|
||||
Assert.AreEqual(321, register.Address);
|
||||
Assert.IsTrue(register.Value > 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetBooleanRegisterFalse()
|
||||
{
|
||||
// Arrange
|
||||
bool value = false;
|
||||
|
||||
// Act
|
||||
var register = value.ToRegister(321);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(register);
|
||||
Assert.AreEqual(321, register.Address);
|
||||
Assert.IsTrue(register.Value == 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetString()
|
||||
{
|
||||
// Arrange
|
||||
string str = "abc";
|
||||
|
||||
// Act
|
||||
var registers = str.ToRegisters(100).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(registers);
|
||||
Assert.AreEqual(2, registers.Count);
|
||||
|
||||
Assert.AreEqual(100, registers[0].Address);
|
||||
Assert.AreEqual(97, registers[0].HighByte);
|
||||
Assert.AreEqual(98, registers[0].LowByte);
|
||||
|
||||
Assert.AreEqual(101, registers[1].Address);
|
||||
Assert.AreEqual(99, registers[1].HighByte);
|
||||
Assert.AreEqual(0, registers[1].LowByte);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetStringReversedRegisters()
|
||||
{
|
||||
// Arrange
|
||||
string str = "abc";
|
||||
|
||||
// Act
|
||||
var registers = str.ToRegisters(100, reverseRegisterOrder: true).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(registers);
|
||||
Assert.AreEqual(2, registers.Count);
|
||||
|
||||
Assert.AreEqual(101, registers[0].Address);
|
||||
Assert.AreEqual(97, registers[0].HighByte);
|
||||
Assert.AreEqual(98, registers[0].LowByte);
|
||||
|
||||
Assert.AreEqual(100, registers[1].Address);
|
||||
Assert.AreEqual(99, registers[1].HighByte);
|
||||
Assert.AreEqual(0, registers[1].LowByte);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetStringReversedBytes()
|
||||
{
|
||||
// Arrange
|
||||
string str = "abc";
|
||||
|
||||
// Act
|
||||
var registers = str.ToRegisters(100, reverseByteOrderPerRegister: true).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(registers);
|
||||
Assert.AreEqual(2, registers.Count);
|
||||
|
||||
Assert.AreEqual(100, registers[0].Address);
|
||||
Assert.AreEqual(97, registers[0].LowByte);
|
||||
Assert.AreEqual(98, registers[0].HighByte);
|
||||
|
||||
Assert.AreEqual(101, registers[1].Address);
|
||||
Assert.AreEqual(99, registers[1].LowByte);
|
||||
Assert.AreEqual(0, registers[1].HighByte);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullOnGetString()
|
||||
{
|
||||
// Arrange
|
||||
string str = null;
|
||||
|
||||
// Act
|
||||
_ = str.ToRegisters(100).ToArray();
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
}
|
||||
|
||||
#endregion Value to Modbus
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
|
||||
{
|
||||
[TestClass]
|
||||
public class ModbusSignedExtensionsTest
|
||||
{
|
||||
#region Modbus to value
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetSByteOnHoldingRegister()
|
||||
{
|
||||
// Arrange
|
||||
var register = new HoldingRegister { Address = 1, HighByte = 0x02, LowByte = 0xFE };
|
||||
|
||||
// Act
|
||||
sbyte sb = register.GetSByte();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(-2, sb);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetSByteOnInputRegister()
|
||||
{
|
||||
// Arrange
|
||||
var register = new InputRegister { Address = 1, HighByte = 0x02, LowByte = 0xFE };
|
||||
|
||||
// Act
|
||||
sbyte sb = register.GetSByte();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(-2, sb);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullForGetSByte()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister register = null;
|
||||
|
||||
// Act
|
||||
register.GetSByte();
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentForGetSByte()
|
||||
{
|
||||
// Arrange
|
||||
var obj = new Coil();
|
||||
|
||||
// Act
|
||||
obj.GetSByte();
|
||||
|
||||
// Assert - ArgumentException
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetInt16OnHoldingRegister()
|
||||
{
|
||||
// Arrange
|
||||
var register = new HoldingRegister { Address = 1, HighByte = 0x02, LowByte = 0x10 };
|
||||
|
||||
// Act
|
||||
short s = register.GetInt16();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(528, s);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetInt16OnInputRegister()
|
||||
{
|
||||
// Arrange
|
||||
var register = new InputRegister { Address = 1, HighByte = 0x02, LowByte = 0x10 };
|
||||
|
||||
// Act
|
||||
short s = register.GetInt16();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(528, s);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullForGetInt16()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister register = null;
|
||||
|
||||
// Act
|
||||
register.GetInt16();
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentForGetInt16()
|
||||
{
|
||||
// Arrange
|
||||
var obj = new Coil();
|
||||
|
||||
// Act
|
||||
obj.GetInt16();
|
||||
|
||||
// Assert - ArgumentException
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetInt32()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new HoldingRegister(),
|
||||
new HoldingRegister { Address = 100, HighByte = 0x01, LowByte = 0x02 },
|
||||
new HoldingRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
int i = registers.GetInt32(1);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(16909060, i);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetInt32ReversedRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new HoldingRegister { Address = 101, HighByte = 0x01, LowByte = 0x02 },
|
||||
new HoldingRegister { Address = 100, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
int i = registers.GetInt32(0, reverseRegisterOrder: true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(16909060, i);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullOnGetInt32()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister[] registers = null;
|
||||
|
||||
// Act
|
||||
registers.GetInt32(0);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetInt32ForLength()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new HoldingRegister { Address = 101, HighByte = 0x01, LowByte = 0x02 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetInt32(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(1)]
|
||||
[DataRow(-1)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowArgumentOutOfRangeOnGetInt32(int startIndex)
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new HoldingRegister { Address = 101, HighByte = 0x01, LowByte = 0x02 },
|
||||
new HoldingRegister { Address = 100, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetInt32(startIndex);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetInt32ForType()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new ModbusObject[]
|
||||
{
|
||||
new HoldingRegister { Address = 100, HighByte = 0x01, LowByte = 0x02 },
|
||||
new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetInt32(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetInt64()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new HoldingRegister(),
|
||||
new HoldingRegister { Address = 100, HighByte = 0x00, LowByte = 0x00 },
|
||||
new HoldingRegister { Address = 101, HighByte = 0x00, LowByte = 0x00 },
|
||||
new HoldingRegister { Address = 102, HighByte = 0x01, LowByte = 0x02 },
|
||||
new HoldingRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
long l = registers.GetInt64(1);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(16909060L, l);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetInt64ReversedRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new HoldingRegister { Address = 103, HighByte = 0x00, LowByte = 0x00 },
|
||||
new HoldingRegister { Address = 102, HighByte = 0x00, LowByte = 0x00 },
|
||||
new HoldingRegister { Address = 101, HighByte = 0x01, LowByte = 0x02 },
|
||||
new HoldingRegister { Address = 100, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
long l = registers.GetInt64(0, reverseRegisterOrder: true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(16909060L, l);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullOnGetInt64()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister[] registers = null;
|
||||
|
||||
// Act
|
||||
registers.GetInt64(0);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetInt64ForLength()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new HoldingRegister { Address = 101, HighByte = 0x00, LowByte = 0x00 },
|
||||
new HoldingRegister { Address = 102, HighByte = 0x01, LowByte = 0x02 },
|
||||
new HoldingRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetInt64(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(1)]
|
||||
[DataRow(-1)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowArgumentOutOfRangeOnGetInt64(int startIndex)
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new HoldingRegister { Address = 100, HighByte = 0x00, LowByte = 0x00 },
|
||||
new HoldingRegister { Address = 101, HighByte = 0x00, LowByte = 0x00 },
|
||||
new HoldingRegister { Address = 102, HighByte = 0x01, LowByte = 0x02 },
|
||||
new HoldingRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetInt64(startIndex);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetInt64ForType()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new ModbusObject[]
|
||||
{
|
||||
new HoldingRegister { Address = 100, HighByte = 0x00, LowByte = 0x00 },
|
||||
new InputRegister { Address = 101, HighByte = 0x00, LowByte = 0x00 },
|
||||
new HoldingRegister { Address = 102, HighByte = 0x01, LowByte = 0x02 },
|
||||
new InputRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetInt64(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
#endregion Modbus to value
|
||||
|
||||
#region Value to Modbus
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldConvertSByte()
|
||||
{
|
||||
// Arrange
|
||||
sbyte sb = -2;
|
||||
|
||||
// Act
|
||||
var register = sb.ToRegister(24);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(register);
|
||||
Assert.AreEqual(24, register.Address);
|
||||
Assert.AreEqual(0x00, register.HighByte);
|
||||
Assert.AreEqual(0xFE, register.LowByte);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldConvertInt16()
|
||||
{
|
||||
// Arrange
|
||||
short s = 1000;
|
||||
|
||||
// Act
|
||||
var register = s.ToRegister(123);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(register);
|
||||
Assert.AreEqual(123, register.Address);
|
||||
Assert.AreEqual(0x03, register.HighByte);
|
||||
Assert.AreEqual(0xE8, register.LowByte);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldConvertInt32()
|
||||
{
|
||||
// Arrange
|
||||
int i = 75000;
|
||||
|
||||
// Act
|
||||
var registers = i.ToRegister(5).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(registers);
|
||||
Assert.AreEqual(2, registers.Count);
|
||||
|
||||
Assert.AreEqual(5, registers[0].Address);
|
||||
Assert.AreEqual(0x00, registers[0].HighByte);
|
||||
Assert.AreEqual(0x01, registers[0].LowByte);
|
||||
|
||||
Assert.AreEqual(6, registers[1].Address);
|
||||
Assert.AreEqual(0x24, registers[1].HighByte);
|
||||
Assert.AreEqual(0xF8, registers[1].LowByte);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldConvertInt32Reversed()
|
||||
{
|
||||
// Arrange
|
||||
int i = 75000;
|
||||
|
||||
// Act
|
||||
var registers = i.ToRegister(5, reverseRegisterOrder: true).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(registers);
|
||||
Assert.AreEqual(2, registers.Count);
|
||||
|
||||
Assert.AreEqual(6, registers[0].Address);
|
||||
Assert.AreEqual(0x00, registers[0].HighByte);
|
||||
Assert.AreEqual(0x01, registers[0].LowByte);
|
||||
|
||||
Assert.AreEqual(5, registers[1].Address);
|
||||
Assert.AreEqual(0x24, registers[1].HighByte);
|
||||
Assert.AreEqual(0xF8, registers[1].LowByte);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldConvertInt64()
|
||||
{
|
||||
// Arrange
|
||||
long l = 75000;
|
||||
|
||||
// Act
|
||||
var registers = l.ToRegister(10).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(registers);
|
||||
Assert.AreEqual(4, registers.Count);
|
||||
|
||||
Assert.AreEqual(10, registers[0].Address);
|
||||
Assert.AreEqual(0x00, registers[0].HighByte);
|
||||
Assert.AreEqual(0x00, registers[0].LowByte);
|
||||
|
||||
Assert.AreEqual(11, registers[1].Address);
|
||||
Assert.AreEqual(0x00, registers[1].HighByte);
|
||||
Assert.AreEqual(0x00, registers[1].LowByte);
|
||||
|
||||
Assert.AreEqual(12, registers[2].Address);
|
||||
Assert.AreEqual(0x00, registers[2].HighByte);
|
||||
Assert.AreEqual(0x01, registers[2].LowByte);
|
||||
|
||||
Assert.AreEqual(13, registers[3].Address);
|
||||
Assert.AreEqual(0x24, registers[3].HighByte);
|
||||
Assert.AreEqual(0xF8, registers[3].LowByte);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldConvertInt64Reversed()
|
||||
{
|
||||
// Arrange
|
||||
long l = 75000;
|
||||
|
||||
// Act
|
||||
var registers = l.ToRegister(10, reverseRegisterOrder: true).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(registers);
|
||||
Assert.AreEqual(4, registers.Count);
|
||||
|
||||
Assert.AreEqual(13, registers[0].Address);
|
||||
Assert.AreEqual(0x00, registers[0].HighByte);
|
||||
Assert.AreEqual(0x00, registers[0].LowByte);
|
||||
|
||||
Assert.AreEqual(12, registers[1].Address);
|
||||
Assert.AreEqual(0x00, registers[1].HighByte);
|
||||
Assert.AreEqual(0x00, registers[1].LowByte);
|
||||
|
||||
Assert.AreEqual(11, registers[2].Address);
|
||||
Assert.AreEqual(0x00, registers[2].HighByte);
|
||||
Assert.AreEqual(0x01, registers[2].LowByte);
|
||||
|
||||
Assert.AreEqual(10, registers[3].Address);
|
||||
Assert.AreEqual(0x24, registers[3].HighByte);
|
||||
Assert.AreEqual(0xF8, registers[3].LowByte);
|
||||
}
|
||||
|
||||
#endregion Value to Modbus
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
|
||||
{
|
||||
[TestClass]
|
||||
public class ModbusUnsignedExtensionsTest
|
||||
{
|
||||
#region Modbus to value
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetByteOnHoldingRegister()
|
||||
{
|
||||
// Arrange
|
||||
var register = new HoldingRegister { Address = 1, HighByte = 0x02, LowByte = 0x10 };
|
||||
|
||||
// Act
|
||||
byte b = register.GetByte();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(16, b);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetByteOnInputRegister()
|
||||
{
|
||||
// Arrange
|
||||
var register = new InputRegister { Address = 1, HighByte = 0x02, LowByte = 0x10 };
|
||||
|
||||
// Act
|
||||
byte b = register.GetByte();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(16, b);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullForGetByte()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister register = null;
|
||||
|
||||
// Act
|
||||
register.GetByte();
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentForGetByte()
|
||||
{
|
||||
// Arrange
|
||||
var obj = new Coil();
|
||||
|
||||
// Act
|
||||
obj.GetByte();
|
||||
|
||||
// Assert - ArgumentException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetUInt16OnHoldingRegister()
|
||||
{
|
||||
// Arrange
|
||||
var register = new HoldingRegister { Address = 1, HighByte = 0x02, LowByte = 0x10 };
|
||||
|
||||
// Act
|
||||
ushort us = register.GetUInt16();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(528, us);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetUInt16OnInputRegister()
|
||||
{
|
||||
// Arrange
|
||||
var register = new InputRegister { Address = 1, HighByte = 0x02, LowByte = 0x10 };
|
||||
|
||||
// Act
|
||||
ushort us = register.GetUInt16();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(528, us);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullForGetUInt16()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister register = null;
|
||||
|
||||
// Act
|
||||
register.GetUInt16();
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentForGetUInt16()
|
||||
{
|
||||
// Arrange
|
||||
var obj = new Coil();
|
||||
|
||||
// Act
|
||||
obj.GetUInt16();
|
||||
|
||||
// Assert - ArgumentException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetUInt32()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new(),
|
||||
new() { Address = 100, HighByte = 0x01, LowByte = 0x02 },
|
||||
new() { Address = 101, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
uint ui = registers.GetUInt32(1);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(16909060u, ui);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetUInt32ReversedRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new() { Address = 101, HighByte = 0x01, LowByte = 0x02 },
|
||||
new() { Address = 100, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
uint ui = registers.GetUInt32(0, reverseRegisterOrder: true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(16909060u, ui);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullOnGetUInt32()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister[] registers = null;
|
||||
|
||||
// Act
|
||||
registers.GetUInt32(0);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetUInt32ForLength()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new() { Address = 101, HighByte = 0x01, LowByte = 0x02 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetUInt32(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(1)]
|
||||
[DataRow(-1)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowArgumentOutOfRangeOnGetUInt32(int startIndex)
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new() { Address = 101, HighByte = 0x01, LowByte = 0x02 },
|
||||
new() { Address = 100, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetUInt32(startIndex);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetUInt32ForType()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new ModbusObject[]
|
||||
{
|
||||
new HoldingRegister { Address = 100, HighByte = 0x01, LowByte = 0x02 },
|
||||
new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetUInt32(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetUInt64()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new(),
|
||||
new() { Address = 100, HighByte = 0x00, LowByte = 0x00 },
|
||||
new() { Address = 101, HighByte = 0x00, LowByte = 0x00 },
|
||||
new() { Address = 102, HighByte = 0x01, LowByte = 0x02 },
|
||||
new() { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
ulong ul = registers.GetUInt64(1);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(16909060ul, ul);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetUInt64ReversedRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new() { Address = 103, HighByte = 0x00, LowByte = 0x00 },
|
||||
new() { Address = 102, HighByte = 0x00, LowByte = 0x00 },
|
||||
new() { Address = 101, HighByte = 0x01, LowByte = 0x02 },
|
||||
new() { Address = 100, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
ulong ul = registers.GetUInt64(0, reverseRegisterOrder: true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(16909060ul, ul);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullOnGetUInt64()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister[] registers = null;
|
||||
|
||||
// Act
|
||||
registers.GetUInt64(0);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetUInt64ForLength()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new() { Address = 101, HighByte = 0x00, LowByte = 0x00 },
|
||||
new() { Address = 102, HighByte = 0x01, LowByte = 0x02 },
|
||||
new() { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetUInt64(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(1)]
|
||||
[DataRow(-1)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowArgumentOutOfRangeOnGetUInt64(int startIndex)
|
||||
{
|
||||
// Arrange
|
||||
var registers = new HoldingRegister[]
|
||||
{
|
||||
new() { Address = 100, HighByte = 0x00, LowByte = 0x00 },
|
||||
new() { Address = 101, HighByte = 0x00, LowByte = 0x00 },
|
||||
new() { Address = 102, HighByte = 0x01, LowByte = 0x02 },
|
||||
new() { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetUInt64(startIndex);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetUInt64ForType()
|
||||
{
|
||||
// Arrange
|
||||
var registers = new ModbusObject[]
|
||||
{
|
||||
new HoldingRegister { Address = 100, HighByte = 0x00, LowByte = 0x00 },
|
||||
new InputRegister { Address = 101, HighByte = 0x00, LowByte = 0x00 },
|
||||
new HoldingRegister { Address = 102, HighByte = 0x01, LowByte = 0x02 },
|
||||
new InputRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetUInt64(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
}
|
||||
|
||||
#endregion Modbus to value
|
||||
|
||||
#region Value to Modbus
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldConvertByte()
|
||||
{
|
||||
// Arrange
|
||||
byte b = 123;
|
||||
|
||||
// Act
|
||||
var register = b.ToRegister(321);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(register);
|
||||
Assert.AreEqual(321, register.Address);
|
||||
Assert.AreEqual(0, register.HighByte);
|
||||
Assert.AreEqual(123, register.LowByte);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldConvertUInt16()
|
||||
{
|
||||
// Arrange
|
||||
ushort us = 1000;
|
||||
|
||||
// Act
|
||||
var register = us.ToRegister(123);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(register);
|
||||
Assert.AreEqual(123, register.Address);
|
||||
Assert.AreEqual(0x03, register.HighByte);
|
||||
Assert.AreEqual(0xE8, register.LowByte);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldConvertUInt32()
|
||||
{
|
||||
// Arrange
|
||||
uint ui = 75000;
|
||||
|
||||
// Act
|
||||
var registers = ui.ToRegister(5).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(registers);
|
||||
Assert.AreEqual(2, registers.Count);
|
||||
|
||||
Assert.AreEqual(5, registers[0].Address);
|
||||
Assert.AreEqual(0x00, registers[0].HighByte);
|
||||
Assert.AreEqual(0x01, registers[0].LowByte);
|
||||
|
||||
Assert.AreEqual(6, registers[1].Address);
|
||||
Assert.AreEqual(0x24, registers[1].HighByte);
|
||||
Assert.AreEqual(0xF8, registers[1].LowByte);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldConvertUInt32Reversed()
|
||||
{
|
||||
// Arrange
|
||||
uint ui = 75000;
|
||||
|
||||
// Act
|
||||
var registers = ui.ToRegister(5, reverseRegisterOrder: true).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(registers);
|
||||
Assert.AreEqual(2, registers.Count);
|
||||
|
||||
Assert.AreEqual(6, registers[0].Address);
|
||||
Assert.AreEqual(0x00, registers[0].HighByte);
|
||||
Assert.AreEqual(0x01, registers[0].LowByte);
|
||||
|
||||
Assert.AreEqual(5, registers[1].Address);
|
||||
Assert.AreEqual(0x24, registers[1].HighByte);
|
||||
Assert.AreEqual(0xF8, registers[1].LowByte);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldConvertUInt64()
|
||||
{
|
||||
// Arrange
|
||||
ulong ul = 75000;
|
||||
|
||||
// Act
|
||||
var registers = ul.ToRegister(10).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(registers);
|
||||
Assert.AreEqual(4, registers.Count);
|
||||
|
||||
Assert.AreEqual(10, registers[0].Address);
|
||||
Assert.AreEqual(0x00, registers[0].HighByte);
|
||||
Assert.AreEqual(0x00, registers[0].LowByte);
|
||||
|
||||
Assert.AreEqual(11, registers[1].Address);
|
||||
Assert.AreEqual(0x00, registers[1].HighByte);
|
||||
Assert.AreEqual(0x00, registers[1].LowByte);
|
||||
|
||||
Assert.AreEqual(12, registers[2].Address);
|
||||
Assert.AreEqual(0x00, registers[2].HighByte);
|
||||
Assert.AreEqual(0x01, registers[2].LowByte);
|
||||
|
||||
Assert.AreEqual(13, registers[3].Address);
|
||||
Assert.AreEqual(0x24, registers[3].HighByte);
|
||||
Assert.AreEqual(0xF8, registers[3].LowByte);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldConvertUInt64Reversed()
|
||||
{
|
||||
// Arrange
|
||||
ulong ul = 75000;
|
||||
|
||||
// Act
|
||||
var registers = ul.ToRegister(10, reverseRegisterOrder: true).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(registers);
|
||||
Assert.AreEqual(4, registers.Count);
|
||||
|
||||
Assert.AreEqual(13, registers[0].Address);
|
||||
Assert.AreEqual(0x00, registers[0].HighByte);
|
||||
Assert.AreEqual(0x00, registers[0].LowByte);
|
||||
|
||||
Assert.AreEqual(12, registers[1].Address);
|
||||
Assert.AreEqual(0x00, registers[1].HighByte);
|
||||
Assert.AreEqual(0x00, registers[1].LowByte);
|
||||
|
||||
Assert.AreEqual(11, registers[2].Address);
|
||||
Assert.AreEqual(0x00, registers[2].HighByte);
|
||||
Assert.AreEqual(0x01, registers[2].LowByte);
|
||||
|
||||
Assert.AreEqual(10, registers[3].Address);
|
||||
Assert.AreEqual(0x24, registers[3].HighByte);
|
||||
Assert.AreEqual(0xF8, registers[3].LowByte);
|
||||
}
|
||||
|
||||
#endregion Value to Modbus
|
||||
}
|
||||
}
|
||||
108
AMWD.Protocols.Modbus.Tests/Common/Models/CoilTest.cs
Normal file
108
AMWD.Protocols.Modbus.Tests/Common/Models/CoilTest.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
namespace AMWD.Protocols.Modbus.Tests.Common.Models
|
||||
{
|
||||
[TestClass]
|
||||
public class CoilTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void ShouldSuccessfulCompare()
|
||||
{
|
||||
// Arrange
|
||||
var coil1 = new Coil { Address = 123, Value = true };
|
||||
var coil2 = new Coil { Address = 123, Value = true };
|
||||
|
||||
// Act
|
||||
bool success = coil1.Equals(coil2);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldFailOnInstanceComparing()
|
||||
{
|
||||
// Arrange
|
||||
var coil1 = new Coil { Address = 123, Value = true };
|
||||
var coil2 = new { Address = 123, HighByte = 0xFF };
|
||||
|
||||
// Act
|
||||
bool success = coil1.Equals(coil2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldFailOnTypeComparing()
|
||||
{
|
||||
// Arrange
|
||||
var coil1 = new Coil { Address = 123, Value = true };
|
||||
var coil2 = new DiscreteInput { Address = 123, HighByte = 0xFF };
|
||||
|
||||
// Act
|
||||
bool success = coil1.Equals(coil2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldFailOnAddressComparing()
|
||||
{
|
||||
// Arrange
|
||||
var coil1 = new Coil { Address = 123, Value = true };
|
||||
var coil2 = new Coil { Address = 321, HighByte = 0xFF };
|
||||
|
||||
// Act
|
||||
bool success = coil1.Equals(coil2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldFailOnHighByteComparing()
|
||||
{
|
||||
// Arrange
|
||||
var coil1 = new Coil { Address = 123, Value = true };
|
||||
var coil2 = new Coil { Address = 123, HighByte = 0x00 };
|
||||
|
||||
// Act
|
||||
bool success = coil1.Equals(coil2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldFailOnLowByteComparing()
|
||||
{
|
||||
// Arrange
|
||||
var coil1 = new Coil { Address = 123, Value = true };
|
||||
var coil2 = new Coil { Address = 123, HighByte = 0xFF, LowByte = 0xFF };
|
||||
|
||||
// Act
|
||||
bool success = coil1.Equals(coil2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(success);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0xFF)]
|
||||
[DataRow(0x00)]
|
||||
public void ShouldPrintPrettyString(int highByte)
|
||||
{
|
||||
// Arrange
|
||||
var coil = new Coil { Address = 123, HighByte = (byte)highByte, LowByte = 0x00 };
|
||||
|
||||
// Act
|
||||
string str = coil.ToString();
|
||||
|
||||
// Assert
|
||||
if (highByte > 0)
|
||||
Assert.AreEqual("Coil #123 | ON", str);
|
||||
else
|
||||
Assert.AreEqual("Coil #123 | OFF", str);
|
||||
}
|
||||
}
|
||||
}
|
||||
108
AMWD.Protocols.Modbus.Tests/Common/Models/DiscreteInputTest.cs
Normal file
108
AMWD.Protocols.Modbus.Tests/Common/Models/DiscreteInputTest.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
namespace AMWD.Protocols.Modbus.Tests.Common.Models
|
||||
{
|
||||
[TestClass]
|
||||
public class DiscreteInputTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void ShouldSuccessfulCompare()
|
||||
{
|
||||
// Arrange
|
||||
var input1 = new DiscreteInput { Address = 123, HighByte = 0xFF, LowByte = 0x00 };
|
||||
var input2 = new DiscreteInput { Address = 123, HighByte = 0xFF, LowByte = 0x00 };
|
||||
|
||||
// Act
|
||||
bool success = input1.Equals(input2);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldFailOnInstanceComparing()
|
||||
{
|
||||
// Arrange
|
||||
var coil1 = new Coil { Address = 123, Value = true };
|
||||
var coil2 = new { Address = 123, HighByte = 0xFF };
|
||||
|
||||
// Act
|
||||
bool success = coil1.Equals(coil2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldFailOnTypeComparing()
|
||||
{
|
||||
// Arrange
|
||||
var input1 = new DiscreteInput { Address = 123, HighByte = 0xFF, LowByte = 0x00 };
|
||||
var input2 = new Coil { Address = 123, HighByte = 0xFF, LowByte = 0x00 };
|
||||
|
||||
// Act
|
||||
bool success = input1.Equals(input2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldFailOnAddressComparing()
|
||||
{
|
||||
// Arrange
|
||||
var input1 = new DiscreteInput { Address = 123, HighByte = 0xFF, LowByte = 0x00 };
|
||||
var input2 = new DiscreteInput { Address = 321, HighByte = 0xFF, LowByte = 0x00 };
|
||||
|
||||
// Act
|
||||
bool success = input1.Equals(input2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldFailOnHighByteComparing()
|
||||
{
|
||||
// Arrange
|
||||
var input1 = new DiscreteInput { Address = 123, HighByte = 0xFF, LowByte = 0x00 };
|
||||
var input2 = new DiscreteInput { Address = 123, HighByte = 0x00, LowByte = 0x00 };
|
||||
|
||||
// Act
|
||||
bool success = input1.Equals(input2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldFailOnLowByteComparing()
|
||||
{
|
||||
// Arrange
|
||||
var input1 = new DiscreteInput { Address = 123, HighByte = 0xFF, LowByte = 0x00 };
|
||||
var input2 = new DiscreteInput { Address = 123, HighByte = 0xFF, LowByte = 0xFF };
|
||||
|
||||
// Act
|
||||
bool success = input1.Equals(input2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(success);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0xFF)]
|
||||
[DataRow(0x00)]
|
||||
public void ShouldPrintPrettyString(int highByte)
|
||||
{
|
||||
// Arrange
|
||||
var input = new DiscreteInput { Address = 123, HighByte = (byte)highByte, LowByte = 0x00 };
|
||||
|
||||
// Act
|
||||
string str = input.ToString();
|
||||
|
||||
// Assert
|
||||
if (highByte > 0)
|
||||
Assert.AreEqual("Discrete Input #123 | ON", str);
|
||||
else
|
||||
Assert.AreEqual("Discrete Input #123 | OFF", str);
|
||||
}
|
||||
}
|
||||
}
|
||||
117
AMWD.Protocols.Modbus.Tests/Common/Models/HoldingRegisterTest.cs
Normal file
117
AMWD.Protocols.Modbus.Tests/Common/Models/HoldingRegisterTest.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
namespace AMWD.Protocols.Modbus.Tests.Common.Models
|
||||
{
|
||||
[TestClass]
|
||||
public class HoldingRegisterTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void ShouldSuccessfulCompare()
|
||||
{
|
||||
// Arrange
|
||||
var register1 = new HoldingRegister { Address = 123, HighByte = 0xBE, LowByte = 0xEF };
|
||||
var register2 = new HoldingRegister { Address = 123, HighByte = 0xBE, LowByte = 0xEF };
|
||||
|
||||
// Act
|
||||
bool success = register1.Equals(register2);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldFailOnInstanceComparing()
|
||||
{
|
||||
// Arrange
|
||||
var register1 = new HoldingRegister { Address = 123, HighByte = 0xBE, LowByte = 0xEF };
|
||||
var register2 = new { Address = 123, HighByte = 0xBE, LowByte = 0xEF };
|
||||
|
||||
// Act
|
||||
bool success = register1.Equals(register2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldFailOnTypeComparing()
|
||||
{
|
||||
// Arrange
|
||||
var register1 = new HoldingRegister { Address = 123, HighByte = 0xBE, LowByte = 0xEF };
|
||||
var register2 = new InputRegister { Address = 123, HighByte = 0xBE, LowByte = 0xEF };
|
||||
|
||||
// Act
|
||||
bool success = register1.Equals(register2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldFailOnAddressComparing()
|
||||
{
|
||||
// Arrange
|
||||
var register1 = new HoldingRegister { Address = 123, HighByte = 0xBE, LowByte = 0xEF };
|
||||
var register2 = new HoldingRegister { Address = 321, HighByte = 0xBE, LowByte = 0xEF };
|
||||
|
||||
// Act
|
||||
bool success = register1.Equals(register2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldFailOnHighByteComparing()
|
||||
{
|
||||
// Arrange
|
||||
var register1 = new HoldingRegister { Address = 123, HighByte = 0xBE, LowByte = 0xEF };
|
||||
var register2 = new HoldingRegister { Address = 123, HighByte = 0xBD, LowByte = 0xEF };
|
||||
|
||||
// Act
|
||||
bool success = register1.Equals(register2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldFailOnLowByteComparing()
|
||||
{
|
||||
// Arrange
|
||||
var register1 = new HoldingRegister { Address = 123, HighByte = 0xBE, LowByte = 0xEF };
|
||||
var register2 = new HoldingRegister { Address = 123, HighByte = 0xBE, LowByte = 0xEE };
|
||||
|
||||
// Act
|
||||
bool success = register1.Equals(register2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldPrintPrettyString()
|
||||
{
|
||||
// Arrange
|
||||
var register = new HoldingRegister { Address = 123, HighByte = 0xBE, LowByte = 0xEF };
|
||||
|
||||
// Act
|
||||
string str = register.ToString();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("Holding Register #123 | 48879 | HI: BE, LO: EF", str);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldSetByValue()
|
||||
{
|
||||
// Arrange
|
||||
var register = new HoldingRegister { Address = 123 };
|
||||
|
||||
// Act
|
||||
register.Value = 48879;
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(0xBE, register.HighByte);
|
||||
Assert.AreEqual(0xEF, register.LowByte);
|
||||
}
|
||||
}
|
||||
}
|
||||
103
AMWD.Protocols.Modbus.Tests/Common/Models/InputRegisterTest.cs
Normal file
103
AMWD.Protocols.Modbus.Tests/Common/Models/InputRegisterTest.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
namespace AMWD.Protocols.Modbus.Tests.Common.Models
|
||||
{
|
||||
[TestClass]
|
||||
public class InputRegisterTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void ShouldSuccessfulCompare()
|
||||
{
|
||||
// Arrange
|
||||
var register1 = new InputRegister { Address = 123, HighByte = 0xBE, LowByte = 0xEF };
|
||||
var register2 = new InputRegister { Address = 123, HighByte = 0xBE, LowByte = 0xEF };
|
||||
|
||||
// Act
|
||||
bool success = register1.Equals(register2);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldFailOnInstanceComparing()
|
||||
{
|
||||
// Arrange
|
||||
var register1 = new InputRegister { Address = 123, HighByte = 0xBE, LowByte = 0xEF };
|
||||
var register2 = new { Address = 123, HighByte = 0xBE, LowByte = 0xEF };
|
||||
|
||||
// Act
|
||||
bool success = register1.Equals(register2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldFailOnTypeComparing()
|
||||
{
|
||||
// Arrange
|
||||
var register1 = new InputRegister { Address = 123, HighByte = 0xBE, LowByte = 0xEF };
|
||||
var register2 = new HoldingRegister { Address = 123, HighByte = 0xBE, LowByte = 0xEF };
|
||||
|
||||
// Act
|
||||
bool success = register1.Equals(register2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldFailOnAddressComparing()
|
||||
{
|
||||
// Arrange
|
||||
var register1 = new InputRegister { Address = 123, HighByte = 0xBE, LowByte = 0xEF };
|
||||
var register2 = new InputRegister { Address = 321, HighByte = 0xBE, LowByte = 0xEF };
|
||||
|
||||
// Act
|
||||
bool success = register1.Equals(register2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldFailOnHighByteComparing()
|
||||
{
|
||||
// Arrange
|
||||
var register1 = new InputRegister { Address = 123, HighByte = 0xBE, LowByte = 0xEF };
|
||||
var register2 = new InputRegister { Address = 123, HighByte = 0xBD, LowByte = 0xEF };
|
||||
|
||||
// Act
|
||||
bool success = register1.Equals(register2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldFailOnLowByteComparing()
|
||||
{
|
||||
// Arrange
|
||||
var register1 = new InputRegister { Address = 123, HighByte = 0xBE, LowByte = 0xEF };
|
||||
var register2 = new InputRegister { Address = 123, HighByte = 0xBE, LowByte = 0xEE };
|
||||
|
||||
// Act
|
||||
bool success = register1.Equals(register2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(success);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldPrintPrettyString()
|
||||
{
|
||||
// Arrange
|
||||
var register = new InputRegister { Address = 123, HighByte = 0xBE, LowByte = 0xEF };
|
||||
|
||||
// Act
|
||||
string str = register.ToString();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("Input Register #123 | 48879 | HI: BE, LO: EF", str);
|
||||
}
|
||||
}
|
||||
}
|
||||
1102
AMWD.Protocols.Modbus.Tests/Common/Protocols/TcpProtocolTest.cs
Normal file
1102
AMWD.Protocols.Modbus.Tests/Common/Protocols/TcpProtocolTest.cs
Normal file
File diff suppressed because it is too large
Load Diff
4
AMWD.Protocols.Modbus.Tests/GlobalUsings.cs
Normal file
4
AMWD.Protocols.Modbus.Tests/GlobalUsings.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
global using AMWD.Protocols.Modbus.Common;
|
||||
global using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
global using System;
|
||||
global using System.Linq;
|
||||
72
AMWD.Protocols.Modbus.sln
Normal file
72
AMWD.Protocols.Modbus.sln
Normal file
@@ -0,0 +1,72 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.8.34525.116
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Common", "AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj", "{2B7689D8-9E56-4DEB-B40E-F70DB4A6F250}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0C43172F-63F3-455A-A5FC-CAE7492A969B}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{A5A9AEA2-3AFF-4536-9FF9-34663DA4D0AD}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
CHANGELOG.md = CHANGELOG.md
|
||||
LICENSE.txt = LICENSE.txt
|
||||
package-icon.png = package-icon.png
|
||||
README.md = README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "config", "config", "{2ED08B2B-1F72-4E1E-9586-1DC6BEFD7BA7}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
.gitignore = .gitignore
|
||||
CodeMaid.config = CodeMaid.config
|
||||
nuget.config = nuget.config
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{C8065AE3-BA87-49AC-8100-C85D6DF7E436}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.gitlab-ci.yml = .gitlab-ci.yml
|
||||
Directory.Build.props = Directory.Build.props
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMWD.Protocols.Modbus.Tests", "AMWD.Protocols.Modbus.Tests\AMWD.Protocols.Modbus.Tests.csproj", "{146070C4-E922-4F5A-AD6F-9A899186E26E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMWD.Protocols.Modbus.Tcp", "AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj", "{8C888A84-CD09-4087-B5DA-67708ABBABA2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMWD.Protocols.Modbus.Serial", "AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj", "{D966826F-EE6C-4BC0-9185-C2A9A50FD586}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{2B7689D8-9E56-4DEB-B40E-F70DB4A6F250}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2B7689D8-9E56-4DEB-B40E-F70DB4A6F250}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2B7689D8-9E56-4DEB-B40E-F70DB4A6F250}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2B7689D8-9E56-4DEB-B40E-F70DB4A6F250}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{146070C4-E922-4F5A-AD6F-9A899186E26E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{146070C4-E922-4F5A-AD6F-9A899186E26E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{146070C4-E922-4F5A-AD6F-9A899186E26E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{146070C4-E922-4F5A-AD6F-9A899186E26E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8C888A84-CD09-4087-B5DA-67708ABBABA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8C888A84-CD09-4087-B5DA-67708ABBABA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8C888A84-CD09-4087-B5DA-67708ABBABA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8C888A84-CD09-4087-B5DA-67708ABBABA2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{A5A9AEA2-3AFF-4536-9FF9-34663DA4D0AD} = {0C43172F-63F3-455A-A5FC-CAE7492A969B}
|
||||
{2ED08B2B-1F72-4E1E-9586-1DC6BEFD7BA7} = {0C43172F-63F3-455A-A5FC-CAE7492A969B}
|
||||
{C8065AE3-BA87-49AC-8100-C85D6DF7E436} = {0C43172F-63F3-455A-A5FC-CAE7492A969B}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {E4FD8EF0-3594-4994-BE80-5FADA5EE17B4}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
31
README.md
31
README.md
@@ -2,36 +2,45 @@
|
||||
|
||||
Here you can find a basic implementation of the Modbus protocol.
|
||||
|
||||
## Package Overview
|
||||
## Overview
|
||||
|
||||
The project is divided into three 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](https://github.com/andreasAmMueller/Modbus)).
|
||||
Only the clients are build very modular to fit any requirement reached on the first implementation back in 2018 ([see here]).
|
||||
The server implementations will only cover their defaults!
|
||||
|
||||
|
||||
|
||||
### Common
|
||||
### [Common]
|
||||
|
||||
Here you'll find all the common interfaces and base implementations for Modbus.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
### Serial
|
||||
### [Serial]
|
||||
|
||||
Here you'll find all the serial protocol implementations.
|
||||
This package contains some wrappers and implementations for the serial protocol.
|
||||
So you can use it out of the box to communicate via serial line ports / devices.
|
||||
|
||||
|
||||
### [TCP]
|
||||
|
||||
### TCP
|
||||
|
||||
Here you'll find all the TCP protocol implementations.
|
||||
|
||||
This package contains the default implementations for network communication via TCP.
|
||||
It uses a specific TCP connection implementation and plugs all things from the Common package together.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Published under [MIT License](LICENSE.txt) (see [**tl;dr**Legal](https://www.tldrlegal.com/license/mit-license))
|
||||
Published under [MIT License] (see [**tl;dr**Legal])
|
||||
|
||||
|
||||
|
||||
[see here]: https://github.com/andreasAMmueller/Modbus
|
||||
[Common]: AMWD.Protocols.Modbus.Common/README.md
|
||||
[Serial]: AMWD.Protocols.Modbus.Serial/README.md
|
||||
[TCP]: AMWD.Protocols.Modbus.Tcp/README.md
|
||||
[MIT License]: LICENSE.txt
|
||||
[**tl;dr**Legal]: https://www.tldrlegal.com/license/mit-license
|
||||
|
||||
Reference in New Issue
Block a user