Implementation of the basic functionallity

This commit is contained in:
2024-02-06 19:47:06 +01:00
parent a6c7828fbe
commit f31f6f94ff
42 changed files with 6875 additions and 11 deletions

159
.editorconfig Normal file
View 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
View 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

View File

@@ -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>

View 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);
}
}

View 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
}
}

View 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");
}
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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();
}
}

View 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);
}
}
}

View 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();
}
}

View File

@@ -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]
};
}
}
}
}

View 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;
}
}
}
}

View File

@@ -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]
};
}
}
}
}

View File

@@ -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]
};
}
}
}
}

View File

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

View 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")}";
}
}

View 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")}";
}
}

View 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}";
}
}

View 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}";
}
}

View 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
}
}
}

View 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
}
}

View 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))

View 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-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>

View 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

View 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>

View 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

View File

@@ -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>

View File

@@ -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; }
}
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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);
}
}
}

File diff suppressed because it is too large Load Diff

View 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
View 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

View File

@@ -2,36 +2,45 @@
Here you can find a basic implementation of the Modbus protocol. Here you can find a basic implementation of the Modbus protocol.
## Package Overview ## Overview
The project is divided into three parts. The project is divided into three parts.
To be mentioned at the beginning: To be mentioned at the beginning:
Only the clients are build very modular to fit any requirement reached on the first implementation back in 2018 ([see here](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! The server implementations will only cover their defaults!
### [Common]
### Common
Here you'll find all the common interfaces and base implementations for Modbus. Here you'll find all the common interfaces and base implementations for Modbus.
For example the default protocol versions: `TCP`, `RTU` and `ASCII`. 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 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.
Here you'll find all the TCP protocol implementations.
--- ---
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