diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..92ed11e
--- /dev/null
+++ b/.editorconfig
@@ -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
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..bd74b86
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -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
diff --git a/AMWD.Protocols.Modbus.Common/AMWD.Protocols.Modbus.Common.csproj b/AMWD.Protocols.Modbus.Common/AMWD.Protocols.Modbus.Common.csproj
new file mode 100644
index 0000000..92ec156
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/AMWD.Protocols.Modbus.Common.csproj
@@ -0,0 +1,14 @@
+
+
+
+ netstandard2.0;net6.0;net8.0
+ 12.0
+
+ amwd-modbus-common
+ AMWD.Protocols.Modbus.Common
+
+ Modbus Protocol Common
+ Common data for Modbus protocol.
+
+
+
diff --git a/AMWD.Protocols.Modbus.Common/Contracts/IModbusConnection.cs b/AMWD.Protocols.Modbus.Common/Contracts/IModbusConnection.cs
new file mode 100644
index 0000000..b412e75
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Contracts/IModbusConnection.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AMWD.Protocols.Modbus.Common.Contracts
+{
+ ///
+ /// Represents a Modbus connection.
+ ///
+ public interface IModbusConnection : IDisposable
+ {
+ ///
+ /// The connection type name.
+ ///
+ string Name { get; }
+
+ ///
+ /// Gets a value indicating whether the connection is open.
+ ///
+ bool IsConnected { get; }
+
+ ///
+ /// Opens the connection to the remote device.
+ ///
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ /// An awaitable .
+ Task ConnectAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Closes the connection to the remote device.
+ ///
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ /// An awaitable .
+ Task DisconnectAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Invokes a Modbus request.
+ ///
+ /// The Modbus request serialized in bytes.
+ /// A function to validate whether the response is complete.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ /// A list of s containing the response.
+ Task> InvokeAsync(IReadOnlyList request, Func, bool> validateResponseComplete, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Contracts/IModbusProtocol.cs b/AMWD.Protocols.Modbus.Common/Contracts/IModbusProtocol.cs
new file mode 100644
index 0000000..860f18d
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Contracts/IModbusProtocol.cs
@@ -0,0 +1,165 @@
+using System.Collections.Generic;
+
+namespace AMWD.Protocols.Modbus.Common.Contracts
+{
+ ///
+ /// A definition of the capabilities an implementation of the Modbus protocol version should have.
+ ///
+ public interface IModbusProtocol
+ {
+ ///
+ /// Gets the protocol type name.
+ ///
+ string Name { get; }
+
+ #region Read
+
+ ///
+ /// Serializes a read request for s.
+ ///
+ /// The unit id.
+ /// The starting address.
+ /// The number of coils to read.
+ /// The s to send.
+ IReadOnlyList SerializeReadCoils(byte unitId, ushort startAddress, ushort count);
+
+ ///
+ /// Deserializes a read response for s.
+ ///
+ /// The s received.
+ /// A list of s.
+ IReadOnlyList DeserializeReadCoils(IReadOnlyList response);
+
+ ///
+ /// Serializes a read request for s.
+ ///
+ /// The unit id.
+ /// The starting address.
+ /// The number of discrete inputs to read.
+ /// The s to send.
+ IReadOnlyList SerializeReadDiscreteInputs(byte unitId, ushort startAddress, ushort count);
+
+ ///
+ /// Deserializes a read response for s.
+ ///
+ /// The s received.
+ /// A list of s.
+ IReadOnlyList DeserializeReadDiscreteInputs(IReadOnlyList response);
+
+ ///
+ /// Serializes a read request for s.
+ ///
+ /// The unit id.
+ /// The starting address.
+ /// The number of holding registers to read.
+ /// The s to send.
+ IReadOnlyList SerializeReadHoldingRegisters(byte unitId, ushort startAddress, ushort count);
+
+ ///
+ /// Deserializes a read response for s.
+ ///
+ /// The s received.
+ /// A list of s.
+ IReadOnlyList DeserializeReadHoldingRegisters(IReadOnlyList response);
+
+ ///
+ /// Serializes a read request for s.
+ ///
+ /// The unit id.
+ /// The starting address.
+ /// The number of input registers to read.
+ /// The s to send.
+ IReadOnlyList SerializeReadInputRegisters(byte unitId, ushort startAddress, ushort count);
+
+ ///
+ /// Deserializes a read response for s.
+ ///
+ /// The s received.
+ /// A list of s.
+ IReadOnlyList DeserializeReadInputRegisters(IReadOnlyList response);
+
+ #endregion Read
+
+ #region Write
+
+ ///
+ /// Serializes a write request for a single .
+ ///
+ /// The unit id.
+ /// The coil to write.
+ /// The s to send.
+ IReadOnlyList SerializeWriteSingleCoil(byte unitId, Coil coil);
+
+ ///
+ /// Deserializes a write response for a single .
+ ///
+ /// The s received.
+ /// Should be the coil itself, as the response is an echo of the request.
+ Coil DeserializeWriteSingleCoil(IReadOnlyList response);
+
+ ///
+ /// Serializes a write request for a single .
+ ///
+ /// The unit id.
+ /// The holding register to write.
+ /// The s to send.
+ IReadOnlyList SerializeWriteSingleHoldingRegister(byte unitId, HoldingRegister register);
+
+ ///
+ /// Deserializes a write response for a single .
+ ///
+ /// The s received.
+ /// Should be the holding register itself, as the response is an echo of the request.
+ HoldingRegister DeserializeWriteSingleHoldingRegister(IReadOnlyList response);
+
+ ///
+ /// Serializes a write request for multiple s.
+ ///
+ /// The unit id.
+ /// The coils to write.
+ /// The s to send.
+ IReadOnlyList SerializeWriteMultipleCoils(byte unitId, IReadOnlyList coils);
+
+ ///
+ /// Deserializes a write response for multiple s.
+ ///
+ /// The s received.
+ /// A tuple containting the first address and the number of coils written.
+ (ushort FirstAddress, ushort NumberOfCoils) DeserializeWriteMultipleCoils(IReadOnlyList response);
+
+ ///
+ /// Serializes a write request for multiple s.
+ ///
+ /// The unit id.
+ /// The holding registers to write.
+ /// The s to send.
+ IReadOnlyList SerializeWriteMultipleHoldingRegisters(byte unitId, IReadOnlyList registers);
+
+ ///
+ /// Deserializes a write response for multiple s.
+ ///
+ /// The s received.
+ /// A tuple containting the first address and the number of holding registers written.
+ (ushort FirstAddress, ushort NumberOfRegisters) DeserializeWriteMultipleHoldingRegisters(IReadOnlyList response);
+
+ #endregion Write
+
+ #region Control
+
+ ///
+ /// Checks whether the receive response bytes are complete to deserialize the response.
+ ///
+ /// The already received response bytes.
+ /// when the response is complete, otherwise .
+ bool CheckResponseComplete(IReadOnlyList responseBytes);
+
+ ///
+ /// Validates the response against the request and throws s if necessary.
+ ///
+ /// The serialized request.
+ /// The received response.
+ void ValidateResponse(IReadOnlyList request, IReadOnlyList response);
+
+ #endregion Control
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Contracts/ModbusClientBase.cs b/AMWD.Protocols.Modbus.Common/Contracts/ModbusClientBase.cs
new file mode 100644
index 0000000..ac5d195
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Contracts/ModbusClientBase.cs
@@ -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
+{
+ ///
+ /// Base implementation of a Modbus client.
+ ///
+ public abstract class ModbusClientBase : IDisposable
+ {
+ private bool _isDisposed;
+
+ ///
+ /// Gets or sets a value indicating whether the connection should be disposed of by .
+ ///
+ protected readonly bool disposeConnection;
+
+ ///
+ /// Gets or sets the responsible for invoking the requests.
+ ///
+ protected readonly IModbusConnection connection;
+
+ ///
+ /// Initializes a new instance of the class with a specific .
+ ///
+ /// The responsible for invoking the requests.
+ public ModbusClientBase(IModbusConnection connection)
+ : this(connection, true)
+ { }
+
+ ///
+ /// Initializes a new instance of the class with a specific .
+ ///
+ /// The responsible for invoking the requests.
+ ///
+ /// if the connection should be disposed of by Dispose(),
+ /// otherwise if you inted to reuse the connection.
+ ///
+ public ModbusClientBase(IModbusConnection connection, bool disposeConnection)
+ {
+ this.connection = connection ?? throw new ArgumentNullException(nameof(connection));
+ this.disposeConnection = disposeConnection;
+ }
+
+ ///
+ /// Gets a value indicating whether the client is connected.
+ ///
+ public bool IsConnected => connection.IsConnected;
+
+ ///
+ /// Gets or sets the protocol type to use.
+ ///
+ ///
+ /// The default protocol used by the client should be initialized in the constructor.
+ ///
+ public abstract IModbusProtocol Protocol { get; set; }
+
+ ///
+ /// Starts the connection to the remote endpoint.
+ ///
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ /// An awaitable .
+ public virtual Task ConnectAsync(CancellationToken cancellationToken = default)
+ {
+ Assertions(false);
+ return connection.ConnectAsync(cancellationToken);
+ }
+
+ ///
+ /// Stops the connection to the remote endpoint.
+ ///
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ /// An awaitable .
+ public virtual Task DisconnectAsync(CancellationToken cancellationToken = default)
+ {
+ Assertions(false);
+ return connection.DisconnectAsync(cancellationToken);
+ }
+
+ ///
+ /// Reads multiple s.
+ ///
+ /// The unit id.
+ /// The starting address.
+ /// The number of coils to read.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ /// A list of s.
+ public virtual async Task> 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();
+ }
+
+ ///
+ /// Reads multiple s.
+ ///
+ /// The unit id.
+ /// The starting address.
+ /// The number of inputs to read.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ /// A list of s.
+ public virtual async Task> 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();
+ }
+
+ ///
+ /// Reads multiple s.
+ ///
+ /// The unit id.
+ /// The starting address.
+ /// The number of registers to read.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ /// A list of s.
+ public virtual async Task> 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;
+ }
+
+ ///
+ /// Reads multiple s.
+ ///
+ /// The unit id.
+ /// The starting address.
+ /// The number of registers to read.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ /// A list of s.
+ public virtual async Task> 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;
+ }
+
+ ///
+ /// Writes a single .
+ ///
+ /// The unit id.
+ /// The coil to write.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ /// on success, otherwise .
+ public virtual async Task 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;
+ }
+
+ ///
+ /// Writs a single .
+ ///
+ /// The unit id.
+ /// The register to write.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ /// on success, otherwise .
+ public virtual async Task 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;
+ }
+
+ ///
+ /// Writes multiple s.
+ ///
+ /// The unit id.
+ /// The coils to write.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ /// on success, otherwise .
+ public virtual async Task WriteMultipleCoilsAsync(byte unitId, IReadOnlyList 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;
+ }
+
+ ///
+ /// Writes multiple s.
+ ///
+ /// The unit id.
+ /// The registers to write.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ /// on success, otherwise .
+ public virtual async Task WriteMultipleHoldingRegistersAsync(byte unitId, IReadOnlyList 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;
+ }
+
+ ///
+ /// Releases all managed and unmanaged resources used by the .
+ ///
+ public virtual void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ public override string ToString()
+ => $"Modbus client using {Protocol.Name} protocol to connect via {connection.Name}";
+
+ ///
+ /// Releases the unmanaged resources used by the
+ /// and optionally also discards the managed resources.
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing && !_isDisposed)
+ {
+ _isDisposed = true;
+
+ if (disposeConnection)
+ connection.Dispose();
+ }
+ }
+
+ ///
+ /// Performs basic assertions.
+ ///
+ 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");
+ }
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Enums/ModbusErrorCode.cs b/AMWD.Protocols.Modbus.Common/Enums/ModbusErrorCode.cs
new file mode 100644
index 0000000..722414d
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Enums/ModbusErrorCode.cs
@@ -0,0 +1,76 @@
+using System.ComponentModel;
+
+namespace AMWD.Protocols.Modbus.Common
+{
+ ///
+ /// List of Modbus exception codes.
+ ///
+ public enum ModbusErrorCode : byte
+ {
+ ///
+ /// No error.
+ ///
+ [Description("No error")]
+ NoError = 0x00,
+
+ ///
+ /// Function code not valid/supported.
+ ///
+ [Description("Illegal function")]
+ IllegalFunction = 0x01,
+
+ ///
+ /// Data address not in range.
+ ///
+ [Description("Illegal data address")]
+ IllegalDataAddress = 0x02,
+
+ ///
+ /// The data value to set is not valid.
+ ///
+ [Description("Illegal data value")]
+ IllegalDataValue = 0x03,
+
+ ///
+ /// Slave device produced a failure.
+ ///
+ [Description("Slave device failure")]
+ SlaveDeviceFailure = 0x04,
+
+ ///
+ /// Ack
+ ///
+ [Description("Acknowledge")]
+ Acknowledge = 0x05,
+
+ ///
+ /// Slave device is working on another task.
+ ///
+ [Description("Slave device busy")]
+ SlaveDeviceBusy = 0x06,
+
+ ///
+ /// nAck
+ ///
+ [Description("Negative acknowledge")]
+ NegativeAcknowledge = 0x07,
+
+ ///
+ /// Momory Parity Error.
+ ///
+ [Description("Memory parity error")]
+ MemoryParityError = 0x08,
+
+ ///
+ /// Gateway of the device could not be reached.
+ ///
+ [Description("Gateway path unavailable")]
+ GatewayPath = 0x0A,
+
+ ///
+ /// Gateway device did no respond.
+ ///
+ [Description("Gateway target device failed to respond")]
+ GatewayTargetDevice = 0x0B
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Enums/ModbusFunctionCode.cs b/AMWD.Protocols.Modbus.Common/Enums/ModbusFunctionCode.cs
new file mode 100644
index 0000000..05ba033
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Enums/ModbusFunctionCode.cs
@@ -0,0 +1,67 @@
+using System.ComponentModel;
+
+namespace AMWD.Protocols.Modbus.Common
+{
+ ///
+ /// List of the Modbus function codes.
+ ///
+ public enum ModbusFunctionCode : byte
+ {
+ ///
+ /// Read coils (Fn 1).
+ ///
+ [Description("Read Coils")]
+ ReadCoils = 0x01,
+
+ ///
+ /// Read discrete inputs (Fn 2).
+ ///
+ [Description("Read Discrete Inputs")]
+ ReadDiscreteInputs = 0x02,
+
+ ///
+ /// Reads holding registers (Fn 3).
+ ///
+ [Description("Read Holding Registers")]
+ ReadHoldingRegisters = 0x03,
+
+ ///
+ /// Reads input registers (Fn 4).
+ ///
+ [Description("Read Input Registers")]
+ ReadInputRegisters = 0x04,
+
+ ///
+ /// Writes a single coil (Fn 5).
+ ///
+ [Description("Write Single Coil")]
+ WriteSingleCoil = 0x05,
+
+ ///
+ /// Writes a single register (Fn 6).
+ ///
+ [Description("Write Single Register")]
+ WriteSingleRegister = 0x06,
+
+ ///
+ /// Writes multiple coils (Fn 15).
+ ///
+ [Description("Write Multiple Coils")]
+ WriteMultipleCoils = 0x0F,
+
+ ///
+ /// Writes multiple registers (Fn 16).
+ ///
+ [Description("Write Multiple Registers")]
+ WriteMultipleRegisters = 0x10,
+
+ ///
+ /// Tunnels service requests and method invocations (Fn 43).
+ ///
+ ///
+ /// This function code needs additional information about its type of request.
+ ///
+ [Description("MODBUS Encapsulated Interface (MEI)")]
+ EncapsulatedInterface = 0x2B
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Enums/ModbusObjectType.cs b/AMWD.Protocols.Modbus.Common/Enums/ModbusObjectType.cs
new file mode 100644
index 0000000..fc63839
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Enums/ModbusObjectType.cs
@@ -0,0 +1,28 @@
+namespace AMWD.Protocols.Modbus.Common
+{
+ ///
+ /// List of specific types.
+ ///
+ public enum ModbusObjectType
+ {
+ ///
+ /// The discrete value is a coil (read/write).
+ ///
+ Coil = 1,
+
+ ///
+ /// The discrete value is an input (read only).
+ ///
+ DiscreteInput = 2,
+
+ ///
+ /// The value is a holding register (read/write).
+ ///
+ HoldingRegister = 3,
+
+ ///
+ /// The value is an input register (read only).
+ ///
+ InputRegister = 4
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Exceptions/ModbusException.cs b/AMWD.Protocols.Modbus.Common/Exceptions/ModbusException.cs
new file mode 100644
index 0000000..377e9f2
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Exceptions/ModbusException.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.Serialization;
+
+namespace AMWD.Protocols.Modbus.Common
+{
+ ///
+ /// Represents errors that occurr during Modbus requests.
+ ///
+ [ExcludeFromCodeCoverage]
+ public class ModbusException : Exception
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ModbusException()
+ : base()
+ { }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with a specified error message.
+ ///
+ /// The message that describes the error.
+ public ModbusException(string message)
+ : base(message)
+ { }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with a specified error message and a reference to the inner exception
+ /// that is the cause of this exception.
+ ///
+ /// The message that describes the error.
+ ///
+ /// The exception that is the cause of the current exception,
+ /// or a null reference if no inner exception is specified.
+ ///
+ public ModbusException(string message, Exception innerException)
+ : base(message, innerException)
+ { }
+
+#if !NET8_0_OR_GREATER
+
+ ///
+ /// Initializes a new instance of the class
+ /// with serialized data.
+ ///
+ ///
+ /// The that holds the serialized
+ /// object data about the exception being thrown.
+ ///
+ ///
+ /// The that contains contextual
+ /// information about the source or destination.
+ ///
+ protected ModbusException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ { }
+
+#endif
+
+ ///
+ /// Gets the Modubs error code.
+ ///
+#if NET6_0_OR_GREATER
+ public ModbusErrorCode ErrorCode { get; init; }
+#else
+ public ModbusErrorCode ErrorCode { get; set; }
+#endif
+
+ ///
+ /// Gets the Modbus error message.
+ ///
+ public string ErrorMessage => ErrorCode.GetDescription();
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs b/AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs
new file mode 100644
index 0000000..de7604d
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs
@@ -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);
+ }
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Extensions/EnumExtensions.cs b/AMWD.Protocols.Modbus.Common/Extensions/EnumExtensions.cs
new file mode 100644
index 0000000..d8e0288
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Extensions/EnumExtensions.cs
@@ -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 GetAttributes(this Enum value)
+ where TAttribute : Attribute
+ {
+ var fieldInfo = value.GetType().GetField(value.ToString());
+ if (fieldInfo == null)
+ return Array.Empty();
+
+ return fieldInfo.GetCustomAttributes(typeof(TAttribute), inherit: false).Cast();
+ }
+
+ private static TAttribute GetAttribute(this Enum value)
+ where TAttribute : Attribute
+ => value.GetAttributes().FirstOrDefault();
+
+ public static string GetDescription(this Enum value)
+ => value.GetAttribute()?.Description ?? value.ToString();
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Extensions/ModbusDecimalExtensions.cs b/AMWD.Protocols.Modbus.Common/Extensions/ModbusDecimalExtensions.cs
new file mode 100644
index 0000000..19cac97
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Extensions/ModbusDecimalExtensions.cs
@@ -0,0 +1,156 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace AMWD.Protocols.Modbus.Common
+{
+ ///
+ /// Custom extensions for s.
+ ///
+ public static class ModbusDecimalExtensions
+ {
+ ///
+ /// Converts multiple s into a value.
+ ///
+ /// The list of Modbus objects.
+ /// The first index to use.
+ /// Indicates whehter the taken registers should be reversed.
+ /// The objects float value.
+ /// when the list is null.
+ /// when the list is too short or the list contains mixed/incompatible objects.
+ /// when the is too high.
+ public static float GetSingle(this IEnumerable 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);
+ }
+
+ ///
+ /// Converts multiple s into a value.
+ ///
+ /// The list of Modbus objects.
+ /// The first index to use.
+ /// Indicates whehter the taken registers should be reversed.
+ /// The objects double value.
+ /// when the list is null.
+ /// when the list is too short or the list contains mixed/incompatible objects.
+ /// when the is too high.
+ public static double GetDouble(this IEnumerable 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);
+ }
+
+ ///
+ /// Converts a value to a list of s.
+ ///
+ /// The float value.
+ /// The first register address.
+ /// Indicates whehter the taken registers should be reversed.
+ /// The list of registers.
+ public static IEnumerable 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]
+ };
+ }
+ }
+
+ ///
+ /// Converts a value to a list of s.
+ ///
+ /// The double value.
+ /// The first register address.
+ /// Indicates whehter the taken registers should be reversed.
+ /// The list of registers.
+ public static IEnumerable 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]
+ };
+ }
+ }
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Extensions/ModbusExtensions.cs b/AMWD.Protocols.Modbus.Common/Extensions/ModbusExtensions.cs
new file mode 100644
index 0000000..9cdde11
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Extensions/ModbusExtensions.cs
@@ -0,0 +1,175 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace AMWD.Protocols.Modbus.Common
+{
+ ///
+ /// Custom extensions for s.
+ ///
+ public static class ModbusExtensions
+ {
+ ///
+ /// Converts a into a value.
+ ///
+ /// The Modbus object.
+ /// The objects bool value.
+ /// when the object is null.
+ 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;
+ }
+
+ ///
+ /// Converts multiple s into a value.
+ ///
+ /// The list of Modbus objects.
+ /// The number of registers to use.
+ /// The first index to use.
+ /// The encoding used to convert the text. (Default: )
+ /// Indicates whehter the taken registers should be reversed.
+ /// Indicates whether to reverse high and low byte per register.
+ /// The objects text value.
+ /// when the list is null.
+ /// when the list is too short or the list contains mixed/incompatible objects.
+ /// when the is too high.
+ public static string GetString(this IEnumerable 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;
+ }
+
+ ///
+ /// Converts a value to a .
+ ///
+ /// The bool value.
+ /// The coil address.
+ /// The coil.
+ public static Coil ToCoil(this bool value, ushort address)
+ {
+ return new Coil
+ {
+ Address = address,
+ Value = value
+ };
+ }
+
+ ///
+ /// Converts a value to a .
+ ///
+ /// The bool value.
+ /// The register address.
+ /// The register.
+ public static HoldingRegister ToRegister(this bool value, ushort address)
+ {
+ return new HoldingRegister
+ {
+ Address = address,
+ Value = (ushort)(value ? 1 : 0)
+ };
+ }
+
+ ///
+ /// Converts a value to a .
+ ///
+ /// The text.
+ /// The address of the text.
+ /// The encoding used to convert the text. (Default: )
+ /// Indicates whehter the taken registers should be reversed.
+ /// Indicates whether to reverse high and low byte per register.
+ /// The registers.
+ /// when the text is null.
+ public static IEnumerable 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;
+ }
+ }
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Extensions/ModbusSignedExtensions.cs b/AMWD.Protocols.Modbus.Common/Extensions/ModbusSignedExtensions.cs
new file mode 100644
index 0000000..d298d35
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Extensions/ModbusSignedExtensions.cs
@@ -0,0 +1,240 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace AMWD.Protocols.Modbus.Common
+{
+ ///
+ /// Custom extensions for s.
+ ///
+ public static class ModbusSignedExtensions
+ {
+ ///
+ /// Converts a into a value.
+ ///
+ /// The Modbus object.
+ /// The objects signed byte value.
+ /// when the object is null.
+ /// when the wrong types are provided.
+ 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));
+ }
+
+ ///
+ /// Converts a into a value.
+ ///
+ /// The Modbus object.
+ /// The objects short value.
+ /// when the object is null.
+ /// when the wrong types are provided.
+ 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));
+ }
+
+ ///
+ /// Converts multiple s into a value.
+ ///
+ /// The list of Modbus objects.
+ /// The first index to use.
+ /// Indicates whehter the taken registers should be reversed.
+ /// The objects int value.
+ /// when the list is null.
+ /// when the list is too short or the list contains mixed/incompatible objects.
+ /// when the is too high.
+ public static int GetInt32(this IEnumerable 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);
+ }
+
+ ///
+ /// Converts multiple s into a value.
+ ///
+ /// The list of Modbus objects.
+ /// The first index to use.
+ /// Indicates whehter the taken registers should be reversed.
+ /// The objects long value.
+ /// when the list is null.
+ /// when the list is too short or the list contains mixed/incompatible objects.
+ /// when the is too high.
+ public static long GetInt64(this IEnumerable 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);
+ }
+
+ ///
+ /// Converts a value to a .
+ ///
+ /// The signed byte value.
+ /// The register address.
+ /// The register.
+ public static HoldingRegister ToRegister(this sbyte value, ushort address)
+ {
+ return new HoldingRegister
+ {
+ Address = address,
+ LowByte = (byte)value
+ };
+ }
+
+ ///
+ /// Converts a value to a .
+ ///
+ /// The short value.
+ /// The register address.
+ /// The register.
+ 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]
+ };
+ }
+
+ ///
+ /// Converts a value to a list of s.
+ ///
+ /// The int value.
+ /// The first register address.
+ /// Indicates whehter the taken registers should be reversed.
+ /// The list of registers.
+ public static IEnumerable 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]
+ };
+ }
+ }
+
+ ///
+ /// Converts a value to a list of s.
+ ///
+ /// The long value.
+ /// The first register address.
+ /// Indicates whehter the taken registers should be reversed.
+ /// The list of registers.
+ public static IEnumerable 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]
+ };
+ }
+ }
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Extensions/ModbusUnsignedExtensions.cs b/AMWD.Protocols.Modbus.Common/Extensions/ModbusUnsignedExtensions.cs
new file mode 100644
index 0000000..72ce0bf
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Extensions/ModbusUnsignedExtensions.cs
@@ -0,0 +1,240 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace AMWD.Protocols.Modbus.Common
+{
+ ///
+ /// Custom extensions for s.
+ ///
+ public static class ModbusUnsignedExtensions
+ {
+ ///
+ /// Converts a into a value.
+ ///
+ /// The Modbus object.
+ /// The objects byte value.
+ /// when the object is null.
+ /// when the wrong types are provided.
+ 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));
+ }
+
+ ///
+ /// Converts a into a value.
+ ///
+ /// The Modbus object.
+ /// The objects unsigned short value.
+ /// when the object is null.
+ /// when the wrong types are provided.
+ 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));
+ }
+
+ ///
+ /// Converts multiple s into a value.
+ ///
+ /// The list of Modbus objects.
+ /// The first index to use.
+ /// Indicates whehter the taken registers should be reversed.
+ /// The objects unsigned int value.
+ /// when the list is null.
+ /// when the list is too short or the list contains mixed/incompatible objects.
+ /// when the is too high.
+ public static uint GetUInt32(this IEnumerable 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);
+ }
+
+ ///
+ /// Converts multiple s into a value.
+ ///
+ /// The list of Modbus objects.
+ /// The first index to use.
+ /// Indicates whehter the taken registers should be reversed.
+ /// The objects unsigned long value.
+ /// when the list is null.
+ /// when the list is too short or the list contains mixed/incompatible objects.
+ /// when the is too high.
+ public static ulong GetUInt64(this IEnumerable 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);
+ }
+
+ ///
+ /// Converts a value to a .
+ ///
+ /// The byte value.
+ /// The register address.
+ /// The register.
+ public static HoldingRegister ToRegister(this byte value, ushort address)
+ {
+ return new HoldingRegister
+ {
+ Address = address,
+ LowByte = value
+ };
+ }
+
+ ///
+ /// Converts a value to a .
+ ///
+ /// The unsigned short value.
+ /// The register address.
+ /// The register.
+ 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]
+ };
+ }
+
+ ///
+ /// Converts a value to a list of s.
+ ///
+ /// The unsigned int value.
+ /// The first register address.
+ /// Indicates whehter the taken registers should be reversed.
+ /// The list of registers.
+ public static IEnumerable 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]
+ };
+ }
+ }
+
+ ///
+ /// Converts a value to a list of s.
+ ///
+ /// The unsigned long value.
+ /// The first register address.
+ /// Indicates whehter the taken registers should be reversed.
+ /// The list of registers.
+ public static IEnumerable 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]
+ };
+ }
+ }
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/InternalsVisibleTo.cs b/AMWD.Protocols.Modbus.Common/InternalsVisibleTo.cs
new file mode 100644
index 0000000..c89850e
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/InternalsVisibleTo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("AMWD.Protocols.Modbus.Tests")]
diff --git a/AMWD.Protocols.Modbus.Common/Models/Coil.cs b/AMWD.Protocols.Modbus.Common/Models/Coil.cs
new file mode 100644
index 0000000..eeab560
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Models/Coil.cs
@@ -0,0 +1,28 @@
+namespace AMWD.Protocols.Modbus.Common
+{
+ ///
+ /// Represents a coil.
+ ///
+ public class Coil : ModbusObject
+ {
+ ///
+ public override ModbusObjectType Type => ModbusObjectType.Coil;
+
+ ///
+ /// Gets or sets a value indicating whether the coil is on or off.
+ ///
+ public bool Value
+ {
+ get => HighByte == 0xFF;
+ set
+ {
+ HighByte = (byte)(value ? 0xFF : 0x00);
+ LowByte = 0x00;
+ }
+ }
+
+ ///
+ public override string ToString()
+ => $"Coil #{Address} | {(Value ? "ON" : "OFF")}";
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Models/DiscreteInput.cs b/AMWD.Protocols.Modbus.Common/Models/DiscreteInput.cs
new file mode 100644
index 0000000..f173ba0
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Models/DiscreteInput.cs
@@ -0,0 +1,20 @@
+namespace AMWD.Protocols.Modbus.Common
+{
+ ///
+ /// Represents a discrete input.
+ ///
+ public class DiscreteInput : ModbusObject
+ {
+ ///
+ public override ModbusObjectType Type => ModbusObjectType.DiscreteInput;
+
+ ///
+ /// Gets or sets a value indicating whether the discrete input is on or off.
+ ///
+ public bool Value => HighByte == 0xFF;
+
+ ///
+ public override string ToString()
+ => $"Discrete Input #{Address} | {(Value ? "ON" : "OFF")}";
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Models/HoldingRegister.cs b/AMWD.Protocols.Modbus.Common/Models/HoldingRegister.cs
new file mode 100644
index 0000000..286131b
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Models/HoldingRegister.cs
@@ -0,0 +1,41 @@
+using System;
+
+namespace AMWD.Protocols.Modbus.Common
+{
+ ///
+ /// Represents a holding register.
+ ///
+ public class HoldingRegister : ModbusObject
+ {
+ ///
+ public override ModbusObjectType Type => ModbusObjectType.HoldingRegister;
+
+ ///
+ /// Gets or sets the value of the holding register.
+ ///
+ 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];
+ }
+ }
+
+ ///
+ public override string ToString()
+ => $"Holding Register #{Address} | {Value} | HI: {HighByte:X2}, LO: {LowByte:X2}";
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Models/InputRegister.cs b/AMWD.Protocols.Modbus.Common/Models/InputRegister.cs
new file mode 100644
index 0000000..795cf1a
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Models/InputRegister.cs
@@ -0,0 +1,32 @@
+using System;
+
+namespace AMWD.Protocols.Modbus.Common
+{
+ ///
+ /// Represents a input register.
+ ///
+ public class InputRegister : ModbusObject
+ {
+ ///
+ public override ModbusObjectType Type => ModbusObjectType.InputRegister;
+
+ ///
+ /// Gets or sets the value of the input register.
+ ///
+ public ushort Value
+ {
+ get
+ {
+ byte[] blob = [HighByte, LowByte];
+ if (BitConverter.IsLittleEndian)
+ Array.Reverse(blob);
+
+ return BitConverter.ToUInt16(blob, 0);
+ }
+ }
+
+ ///
+ public override string ToString()
+ => $"Input Register #{Address} | {Value} | HI: {HighByte:X2}, LO: {LowByte:X2}";
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Models/ModbusObject.cs b/AMWD.Protocols.Modbus.Common/Models/ModbusObject.cs
new file mode 100644
index 0000000..c7b91b0
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Models/ModbusObject.cs
@@ -0,0 +1,56 @@
+using System;
+
+namespace AMWD.Protocols.Modbus.Common
+{
+ ///
+ /// Represents the base of all Modbus specific objects.
+ ///
+ public abstract class ModbusObject
+ {
+ ///
+ /// Gets the type of the object.
+ ///
+ public abstract ModbusObjectType Type { get; }
+
+ ///
+ /// Gets or sets the address of the object.
+ ///
+ public ushort Address { get; set; }
+
+ ///
+ /// Gets or sets the high byte of the value.
+ ///
+ public byte HighByte { get; set; }
+
+ ///
+ /// Gets or sets the low byte of the value.
+ ///
+ public byte LowByte { get; set; }
+
+ ///
+ 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;
+ }
+
+ ///
+ [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
+ }
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs b/AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs
new file mode 100644
index 0000000..91ab546
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs
@@ -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
+{
+ ///
+ /// Default implementation of the Modbus TCP protocol.
+ ///
+ public class TcpProtocol : IModbusProtocol
+ {
+ #region Fields
+
+ private readonly object _lock = new();
+ private ushort _transactionId = 0x0000;
+
+ #endregion Fields
+
+ #region Constants
+
+ ///
+ /// The minimum allowed unit id specified by the Modbus TCP protocol.
+ ///
+ public const byte MIN_UNIT_ID = 0x00;
+
+ ///
+ /// The maximum allowed unit id specified by the Modbus TCP protocol.
+ ///
+ public const byte MAX_UNIT_ID = 0xFF;
+
+ ///
+ /// The minimum allowed read count specified by the Modbus TCP protocol.
+ ///
+ public const ushort MIN_READ_COUNT = 0x01;
+
+ ///
+ /// The minimum allowed write count specified by the Modbus TCP protocol.
+ ///
+ public const ushort MIN_WRITE_COUNT = 0x01;
+
+ ///
+ /// The maximum allowed read count for discrete values specified by the Modbus TCP protocol.
+ ///
+ public const ushort MAX_DISCRETE_READ_COUNT = 0x07D0; // 2000
+
+ ///
+ /// The maximum allowed write count for discrete values specified by the Modbus TCP protocol.
+ ///
+ public const ushort MAX_DISCRETE_WRITE_COUNT = 0x07B0; // 1968
+
+ ///
+ /// The maximum allowed read count for registers specified by the Modbus TCP protocol.
+ ///
+ public const ushort MAX_REGISTER_READ_COUNT = 0x007D; // 125
+
+ ///
+ /// The maximum allowed write count for registers specified by the Modbus TCP protocol.
+ ///
+ public const ushort MAX_REGISTER_WRITE_COUNT = 0x007B; // 123
+
+ #endregion Constants
+
+ ///
+ public string Name => "TCP";
+
+ ///
+ /// Gets or sets a value indicating whether to disable the transaction id usage.
+ ///
+ public bool DisableTransactionId { get; set; }
+
+ #region Read
+
+ ///
+ public IReadOnlyList 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;
+ }
+
+ ///
+ public IReadOnlyList DeserializeReadCoils(IReadOnlyList 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();
+ 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;
+ }
+
+ ///
+ public IReadOnlyList 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;
+ }
+
+ ///
+ public IReadOnlyList DeserializeReadDiscreteInputs(IReadOnlyList 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();
+ 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;
+ }
+
+ ///
+ public IReadOnlyList 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;
+ }
+
+ ///
+ public IReadOnlyList DeserializeReadHoldingRegisters(IReadOnlyList 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();
+ 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;
+ }
+
+ ///
+ public IReadOnlyList 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;
+ }
+
+ ///
+ public IReadOnlyList DeserializeReadInputRegisters(IReadOnlyList 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();
+ 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
+
+ ///
+ public IReadOnlyList 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;
+ }
+
+ ///
+ public Coil DeserializeWriteSingleCoil(IReadOnlyList response)
+ {
+ return new Coil
+ {
+ Address = ToNetworkUInt16(response.Skip(8).Take(2).ToArray()),
+ HighByte = response[10],
+ LowByte = response[11]
+ };
+ }
+
+ ///
+ public IReadOnlyList 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;
+ }
+
+ ///
+ public HoldingRegister DeserializeWriteSingleHoldingRegister(IReadOnlyList response)
+ {
+ return new HoldingRegister
+ {
+ Address = ToNetworkUInt16(response.Skip(8).Take(2).ToArray()),
+ HighByte = response[10],
+ LowByte = response[11]
+ };
+ }
+
+ ///
+ public IReadOnlyList SerializeWriteMultipleCoils(byte unitId, IReadOnlyList 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;
+ }
+
+ ///
+ public (ushort FirstAddress, ushort NumberOfCoils) DeserializeWriteMultipleCoils(IReadOnlyList response)
+ {
+ ushort firstAddress = ToNetworkUInt16(response.Skip(8).Take(2).ToArray());
+ ushort numberOfCoils = ToNetworkUInt16(response.Skip(10).Take(2).ToArray());
+
+ return (firstAddress, numberOfCoils);
+ }
+
+ ///
+ public IReadOnlyList SerializeWriteMultipleHoldingRegisters(byte unitId, IReadOnlyList 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;
+ }
+
+ ///
+ public (ushort FirstAddress, ushort NumberOfRegisters) DeserializeWriteMultipleHoldingRegisters(IReadOnlyList 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
+
+ ///
+ public bool CheckResponseComplete(IReadOnlyList 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;
+ }
+
+ ///
+ public void ValidateResponse(IReadOnlyList request, IReadOnlyList 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
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Common/README.md b/AMWD.Protocols.Modbus.Common/README.md
new file mode 100644
index 0000000..6e24772
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Common/README.md
@@ -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))
diff --git a/AMWD.Protocols.Modbus.Serial/AMWD.Protocols.Modbus.Serial.csproj b/AMWD.Protocols.Modbus.Serial/AMWD.Protocols.Modbus.Serial.csproj
new file mode 100644
index 0000000..43d012f
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Serial/AMWD.Protocols.Modbus.Serial.csproj
@@ -0,0 +1,18 @@
+
+
+
+ netstandard2.0;net6.0;net8.0
+ 12.0
+
+ amwd-modbus-serial
+ AMWD.Protocols.Modbus.Serial
+
+ Modbus RTU/ASCII Protocol
+ Implementation of the Modbus protocol communicating via serial line using RTU or ASCII encoding.
+
+
+
+
+
+
+
diff --git a/AMWD.Protocols.Modbus.Serial/README.md b/AMWD.Protocols.Modbus.Serial/README.md
new file mode 100644
index 0000000..683feab
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Serial/README.md
@@ -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
diff --git a/AMWD.Protocols.Modbus.Tcp/AMWD.Protocols.Modbus.Tcp.csproj b/AMWD.Protocols.Modbus.Tcp/AMWD.Protocols.Modbus.Tcp.csproj
new file mode 100644
index 0000000..a2808bf
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Tcp/AMWD.Protocols.Modbus.Tcp.csproj
@@ -0,0 +1,18 @@
+
+
+
+ netstandard2.0;net6.0;net8.0
+ 12.0
+
+ amwd-modbus-tcp
+ AMWD.Protocols.Modbus.Tcp
+
+ Modbus TCP Protocol
+ Implementation of the Modbus protocol communicating via TCP.
+
+
+
+
+
+
+
diff --git a/AMWD.Protocols.Modbus.Tcp/README.md b/AMWD.Protocols.Modbus.Tcp/README.md
new file mode 100644
index 0000000..db54acf
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Tcp/README.md
@@ -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
diff --git a/AMWD.Protocols.Modbus.Tests/AMWD.Protocols.Modbus.Tests.csproj b/AMWD.Protocols.Modbus.Tests/AMWD.Protocols.Modbus.Tests.csproj
new file mode 100644
index 0000000..e1fa925
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Tests/AMWD.Protocols.Modbus.Tests.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net8.0
+ 12.0
+
+ false
+ true
+ true
+
+ false
+ false
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AMWD.Protocols.Modbus.Tests/Common/Contracts/ModbusClientBaseTest.cs b/AMWD.Protocols.Modbus.Tests/Common/Contracts/ModbusClientBaseTest.cs
new file mode 100644
index 0000000..8ee01c6
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Tests/Common/Contracts/ModbusClientBaseTest.cs
@@ -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 _connection;
+ private Mock _protocol;
+
+ // Responses
+ private bool _connectionIsConnectecd;
+ private List _readCoilsResponse;
+ private List _readDiscreteInputsResponse;
+ private List _readHoldingRegistersResponse;
+ private List _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();
+ _readDiscreteInputsResponse = new List();
+ _readHoldingRegistersResponse = new List();
+ _readInputRegistersResponse = new List();
+
+ 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()), 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()), 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>(), It.IsAny, bool>>(), It.IsAny()), Times.Once);
+ _connection.VerifyNoOtherCalls();
+
+ _protocol.Verify(p => p.SerializeReadCoils(UNIT_ID, START_ADDRESS, READ_COUNT), Times.Once);
+ _protocol.Verify(p => p.ValidateResponse(It.IsAny>(), It.IsAny>()), Times.Once);
+ _protocol.Verify(p => p.DeserializeReadCoils(It.IsAny>()), 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>(), It.IsAny, bool>>(), It.IsAny()), Times.Once);
+ _connection.VerifyNoOtherCalls();
+
+ _protocol.Verify(p => p.SerializeReadDiscreteInputs(UNIT_ID, START_ADDRESS, READ_COUNT), Times.Once);
+ _protocol.Verify(p => p.ValidateResponse(It.IsAny>(), It.IsAny>()), Times.Once);
+ _protocol.Verify(p => p.DeserializeReadDiscreteInputs(It.IsAny>()), 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>(), It.IsAny, bool>>(), It.IsAny()), Times.Once);
+ _connection.VerifyNoOtherCalls();
+
+ _protocol.Verify(p => p.SerializeReadHoldingRegisters(UNIT_ID, START_ADDRESS, READ_COUNT), Times.Once);
+ _protocol.Verify(p => p.ValidateResponse(It.IsAny>(), It.IsAny>()), Times.Once);
+ _protocol.Verify(p => p.DeserializeReadHoldingRegisters(It.IsAny>()), 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>(), It.IsAny, bool>>(), It.IsAny()), Times.Once);
+ _connection.VerifyNoOtherCalls();
+
+ _protocol.Verify(p => p.SerializeReadInputRegisters(UNIT_ID, START_ADDRESS, READ_COUNT), Times.Once);
+ _protocol.Verify(p => p.ValidateResponse(It.IsAny>(), It.IsAny>()), Times.Once);
+ _protocol.Verify(p => p.DeserializeReadInputRegisters(It.IsAny>()), 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>(), It.IsAny, bool>>(), It.IsAny()), Times.Once);
+ _connection.VerifyNoOtherCalls();
+
+ _protocol.Verify(p => p.SerializeWriteSingleCoil(UNIT_ID, coil), Times.Once);
+ _protocol.Verify(p => p.ValidateResponse(It.IsAny>(), It.IsAny>()), Times.Once);
+ _protocol.Verify(p => p.DeserializeWriteSingleCoil(It.IsAny>()), 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>(), It.IsAny, bool>>(), It.IsAny()), Times.Once);
+ _connection.VerifyNoOtherCalls();
+
+ _protocol.Verify(p => p.SerializeWriteSingleCoil(UNIT_ID, coil), Times.Once);
+ _protocol.Verify(p => p.ValidateResponse(It.IsAny>(), It.IsAny>()), Times.Once);
+ _protocol.Verify(p => p.DeserializeWriteSingleCoil(It.IsAny>()), 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>(), It.IsAny, bool>>(), It.IsAny()), Times.Once);
+ _connection.VerifyNoOtherCalls();
+
+ _protocol.Verify(p => p.SerializeWriteSingleCoil(UNIT_ID, coil), Times.Once);
+ _protocol.Verify(p => p.ValidateResponse(It.IsAny>(), It.IsAny>()), Times.Once);
+ _protocol.Verify(p => p.DeserializeWriteSingleCoil(It.IsAny>()), 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>(), It.IsAny, bool>>(), It.IsAny()), Times.Once);
+ _connection.VerifyNoOtherCalls();
+
+ _protocol.Verify(p => p.SerializeWriteSingleHoldingRegister(UNIT_ID, register), Times.Once);
+ _protocol.Verify(p => p.ValidateResponse(It.IsAny>(), It.IsAny>()), Times.Once);
+ _protocol.Verify(p => p.DeserializeWriteSingleHoldingRegister(It.IsAny>()), 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>(), It.IsAny, bool>>(), It.IsAny()), Times.Once);
+ _connection.VerifyNoOtherCalls();
+
+ _protocol.Verify(p => p.SerializeWriteSingleHoldingRegister(UNIT_ID, register), Times.Once);
+ _protocol.Verify(p => p.ValidateResponse(It.IsAny>(), It.IsAny>()), Times.Once);
+ _protocol.Verify(p => p.DeserializeWriteSingleHoldingRegister(It.IsAny>()), 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>(), It.IsAny, bool>>(), It.IsAny()), Times.Once);
+ _connection.VerifyNoOtherCalls();
+
+ _protocol.Verify(p => p.SerializeWriteSingleHoldingRegister(UNIT_ID, register), Times.Once);
+ _protocol.Verify(p => p.ValidateResponse(It.IsAny>(), It.IsAny>()), Times.Once);
+ _protocol.Verify(p => p.DeserializeWriteSingleHoldingRegister(It.IsAny>()), Times.Once);
+ _protocol.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ public async Task ShouldWriteMultipleCoils()
+ {
+ // Arrange
+ var coils = new List();
+ 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>(), It.IsAny, bool>>(), It.IsAny()), Times.Once);
+ _connection.VerifyNoOtherCalls();
+
+ _protocol.Verify(p => p.SerializeWriteMultipleCoils(UNIT_ID, coils), Times.Once);
+ _protocol.Verify(p => p.ValidateResponse(It.IsAny>(), It.IsAny>()), Times.Once);
+ _protocol.Verify(p => p.DeserializeWriteMultipleCoils(It.IsAny>()), Times.Once);
+ _protocol.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ public async Task ShouldFailWriteMultipleCoilsOnAddress()
+ {
+ // Arrange
+ _writeMultipleCoilsResponse.startAddress = START_ADDRESS + 1;
+ var coils = new List();
+ 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>(), It.IsAny, bool>>(), It.IsAny()), Times.Once);
+ _connection.VerifyNoOtherCalls();
+
+ _protocol.Verify(p => p.SerializeWriteMultipleCoils(UNIT_ID, coils), Times.Once);
+ _protocol.Verify(p => p.ValidateResponse(It.IsAny>(), It.IsAny>()), Times.Once);
+ _protocol.Verify(p => p.DeserializeWriteMultipleCoils(It.IsAny>()), Times.Once);
+ _protocol.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ public async Task ShouldFailWriteMultipleCoilsOnCount()
+ {
+ // Arrange
+ _writeMultipleCoilsResponse.count = READ_COUNT + 1;
+ var coils = new List();
+ 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>(), It.IsAny, bool>>(), It.IsAny()), Times.Once);
+ _connection.VerifyNoOtherCalls();
+
+ _protocol.Verify(p => p.SerializeWriteMultipleCoils(UNIT_ID, coils), Times.Once);
+ _protocol.Verify(p => p.ValidateResponse(It.IsAny>(), It.IsAny>()), Times.Once);
+ _protocol.Verify(p => p.DeserializeWriteMultipleCoils(It.IsAny>()), Times.Once);
+ _protocol.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ public async Task ShouldWriteMultipleRegisters()
+ {
+ // Arrange
+ var registers = new List();
+ 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>(), It.IsAny, bool>>(), It.IsAny()), Times.Once);
+ _connection.VerifyNoOtherCalls();
+
+ _protocol.Verify(p => p.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers), Times.Once);
+ _protocol.Verify(p => p.ValidateResponse(It.IsAny>(), It.IsAny>()), Times.Once);
+ _protocol.Verify(p => p.DeserializeWriteMultipleHoldingRegisters(It.IsAny>()), Times.Once);
+ _protocol.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ public async Task ShouldFailWriteMultiplRegistersOnAddress()
+ {
+ // Arrange
+ _writeMultipleHoldingRegistersResponse.startAddress = START_ADDRESS + 1;
+ var registers = new List();
+ 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>(), It.IsAny, bool>>(), It.IsAny()), Times.Once);
+ _connection.VerifyNoOtherCalls();
+
+ _protocol.Verify(p => p.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers), Times.Once);
+ _protocol.Verify(p => p.ValidateResponse(It.IsAny>(), It.IsAny>()), Times.Once);
+ _protocol.Verify(p => p.DeserializeWriteMultipleHoldingRegisters(It.IsAny>()), Times.Once);
+ _protocol.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ public async Task ShouldFailWriteMultipleRegistersOnCount()
+ {
+ // Arrange
+ _writeMultipleHoldingRegistersResponse.count = READ_COUNT + 1;
+ var registers = new List();
+ 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>(), It.IsAny, bool>>(), It.IsAny()), Times.Once);
+ _connection.VerifyNoOtherCalls();
+
+ _protocol.Verify(p => p.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers), Times.Once);
+ _protocol.Verify(p => p.ValidateResponse(It.IsAny>(), It.IsAny>()), Times.Once);
+ _protocol.Verify(p => p.DeserializeWriteMultipleHoldingRegisters(It.IsAny>()), Times.Once);
+ _protocol.VerifyNoOtherCalls();
+ }
+
+ private ModbusClientBase GetClient(bool disposeConnection = true)
+ {
+ _connection = new Mock();
+ _connection
+ .SetupGet(c => c.Name)
+ .Returns("Mock");
+ _connection
+ .SetupGet(c => c.IsConnected)
+ .Returns(() => _connectionIsConnectecd);
+
+ _protocol = new Mock();
+ _protocol
+ .SetupGet(p => p.Name)
+ .Returns("Moq");
+ _protocol
+ .Setup(p => p.DeserializeReadCoils(It.IsAny>()))
+ .Returns(() => _readCoilsResponse);
+ _protocol
+ .Setup(p => p.DeserializeReadDiscreteInputs(It.IsAny>()))
+ .Returns(() => _readDiscreteInputsResponse);
+ _protocol
+ .Setup(p => p.DeserializeReadHoldingRegisters(It.IsAny>()))
+ .Returns(() => _readHoldingRegistersResponse);
+ _protocol
+ .Setup(p => p.DeserializeReadInputRegisters(It.IsAny>()))
+ .Returns(() => _readInputRegistersResponse);
+
+ _protocol
+ .Setup(p => p.DeserializeWriteSingleCoil(It.IsAny>()))
+ .Returns(() => _writeSingleCoilResponse);
+ _protocol
+ .Setup(p => p.DeserializeWriteSingleHoldingRegister(It.IsAny>()))
+ .Returns(() => _writeSingleHoldingRegisterResponse);
+ _protocol
+ .Setup(p => p.DeserializeWriteMultipleCoils(It.IsAny>()))
+ .Returns(() => _writeMultipleCoilsResponse);
+ _protocol
+ .Setup(p => p.DeserializeWriteMultipleHoldingRegisters(It.IsAny>()))
+ .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; }
+ }
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Tests/Common/Extensions/ModbusDecimalExtensionsTest.cs b/AMWD.Protocols.Modbus.Tests/Common/Extensions/ModbusDecimalExtensionsTest.cs
new file mode 100644
index 0000000..db088cc
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Tests/Common/Extensions/ModbusDecimalExtensionsTest.cs
@@ -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
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Tests/Common/Extensions/ModbusExtensionsTest.cs b/AMWD.Protocols.Modbus.Tests/Common/Extensions/ModbusExtensionsTest.cs
new file mode 100644
index 0000000..cc24e7a
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Tests/Common/Extensions/ModbusExtensionsTest.cs
@@ -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();
+
+ // 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
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Tests/Common/Extensions/ModbusSignedExtensionsTest.cs b/AMWD.Protocols.Modbus.Tests/Common/Extensions/ModbusSignedExtensionsTest.cs
new file mode 100644
index 0000000..b1c4ec8
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Tests/Common/Extensions/ModbusSignedExtensionsTest.cs
@@ -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
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Tests/Common/Extensions/ModbusUnsignedExtensionsTest.cs b/AMWD.Protocols.Modbus.Tests/Common/Extensions/ModbusUnsignedExtensionsTest.cs
new file mode 100644
index 0000000..d521ba1
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Tests/Common/Extensions/ModbusUnsignedExtensionsTest.cs
@@ -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
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Tests/Common/Models/CoilTest.cs b/AMWD.Protocols.Modbus.Tests/Common/Models/CoilTest.cs
new file mode 100644
index 0000000..37e7bcb
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Tests/Common/Models/CoilTest.cs
@@ -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);
+ }
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Tests/Common/Models/DiscreteInputTest.cs b/AMWD.Protocols.Modbus.Tests/Common/Models/DiscreteInputTest.cs
new file mode 100644
index 0000000..e41fffc
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Tests/Common/Models/DiscreteInputTest.cs
@@ -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);
+ }
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Tests/Common/Models/HoldingRegisterTest.cs b/AMWD.Protocols.Modbus.Tests/Common/Models/HoldingRegisterTest.cs
new file mode 100644
index 0000000..66a2a54
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Tests/Common/Models/HoldingRegisterTest.cs
@@ -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);
+ }
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Tests/Common/Models/InputRegisterTest.cs b/AMWD.Protocols.Modbus.Tests/Common/Models/InputRegisterTest.cs
new file mode 100644
index 0000000..d93fb0c
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Tests/Common/Models/InputRegisterTest.cs
@@ -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);
+ }
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Tests/Common/Protocols/TcpProtocolTest.cs b/AMWD.Protocols.Modbus.Tests/Common/Protocols/TcpProtocolTest.cs
new file mode 100644
index 0000000..253c976
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Tests/Common/Protocols/TcpProtocolTest.cs
@@ -0,0 +1,1102 @@
+using System.Collections.Generic;
+using System.Reflection;
+using AMWD.Protocols.Modbus.Common.Protocols;
+
+namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
+{
+ [TestClass]
+ public class TcpProtocolTest
+ {
+ private const byte UNIT_ID = 0x2A; // 42
+
+ #region Read Coils
+
+ [TestMethod]
+ public void ShouldSerializeReadCoils()
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ var bytes = protocol.SerializeReadCoils(UNIT_ID, 19, 19);
+
+ // Assert
+ Assert.IsNotNull(bytes);
+ Assert.AreEqual(12, bytes.Count);
+
+ // Transaction id
+ Assert.AreEqual(0x00, bytes[0]);
+ Assert.AreEqual(0x01, bytes[1]);
+
+ // Protocol identifier
+ Assert.AreEqual(0x00, bytes[2]);
+ Assert.AreEqual(0x00, bytes[3]);
+
+ // Following bytes
+ Assert.AreEqual(0x00, bytes[4]);
+ Assert.AreEqual(0x06, bytes[5]);
+
+ // Unit id
+ Assert.AreEqual(UNIT_ID, bytes[6]);
+
+ // Function code
+ Assert.AreEqual(0x01, bytes[7]);
+
+ // Starting address
+ Assert.AreEqual(0x00, bytes[8]);
+ Assert.AreEqual(0x13, bytes[9]);
+ // Quantity
+ Assert.AreEqual(0x00, bytes[10]);
+ Assert.AreEqual(0x13, bytes[11]);
+ }
+
+ [DataTestMethod]
+ [DataRow(0)]
+ [DataRow(2001)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForCountOnSerializeReadCoils(int count)
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.SerializeReadCoils(UNIT_ID, 19, (ushort)count);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadCoils()
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.SerializeReadCoils(UNIT_ID, ushort.MaxValue, 2);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ public void ShouldDeserializeReadCoils()
+ {
+ // Arrange
+ int[] setValues = [0, 2, 3, 6, 7, 8, 9, 11, 13, 14, 16, 18];
+ var protocol = new TcpProtocol();
+
+ // Act
+ var coils = protocol.DeserializeReadCoils([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x01, 0x03, 0xCD, 0x6B, 0x05]);
+
+ // Assert
+ Assert.IsNotNull(coils);
+ Assert.AreEqual(24, coils.Count);
+
+ for (int i = 0; i < 24; i++)
+ {
+ Assert.AreEqual(i, coils[i].Address);
+
+ if (setValues.Contains(i))
+ Assert.IsTrue(coils[i].Value);
+ else
+ Assert.IsFalse(coils[i].Value);
+ }
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowExceptionOnDeserializeReadCoils()
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ var coils = protocol.DeserializeReadCoils([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x01, 0x02, 0xCD, 0x6B, 0x05]);
+
+ // Assert - ModbusException
+ }
+
+ #endregion Read Coils
+
+ #region Read Discrete Inputs
+
+ [TestMethod]
+ public void ShouldSerializeReadDiscreteInputs()
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ var bytes = protocol.SerializeReadDiscreteInputs(UNIT_ID, 260, 16);
+
+ // Assert
+ Assert.IsNotNull(bytes);
+ Assert.AreEqual(12, bytes.Count);
+
+ // Transaction id
+ Assert.AreEqual(0x00, bytes[0]);
+ Assert.AreEqual(0x01, bytes[1]);
+
+ // Protocol identifier
+ Assert.AreEqual(0x00, bytes[2]);
+ Assert.AreEqual(0x00, bytes[3]);
+
+ // Following bytes
+ Assert.AreEqual(0x00, bytes[4]);
+ Assert.AreEqual(0x06, bytes[5]);
+
+ // Unit id
+ Assert.AreEqual(UNIT_ID, bytes[6]);
+
+ // Function code
+ Assert.AreEqual(0x02, bytes[7]);
+
+ // Starting address
+ Assert.AreEqual(0x01, bytes[8]);
+ Assert.AreEqual(0x04, bytes[9]);
+ // Quantity
+ Assert.AreEqual(0x00, bytes[10]);
+ Assert.AreEqual(0x10, bytes[11]);
+ }
+
+ [DataTestMethod]
+ [DataRow(0)]
+ [DataRow(2001)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForCountOnSerializeReadDiscreteInputs(int count)
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, (ushort)count);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadDiscreteInputs()
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.SerializeReadDiscreteInputs(UNIT_ID, ushort.MaxValue, 2);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ public void ShouldDeserializeReadDiscreteInputs()
+ {
+ // Arrange
+ int[] setValues = [0, 2, 3, 6, 7, 8, 9, 11, 13, 14, 16, 17];
+ var protocol = new TcpProtocol();
+
+ // Act
+ var inputs = protocol.DeserializeReadDiscreteInputs([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x02, 0x03, 0xCD, 0x6B, 0x03]);
+
+ // Assert
+ Assert.IsNotNull(inputs);
+ Assert.AreEqual(24, inputs.Count);
+
+ for (int i = 0; i < 24; i++)
+ {
+ Assert.AreEqual(i, inputs[i].Address);
+
+ if (setValues.Contains(i))
+ Assert.IsTrue(inputs[i].Value);
+ else
+ Assert.IsFalse(inputs[i].Value);
+ }
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowExceptionOnDeserializeReadDiscreteInputs()
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.DeserializeReadDiscreteInputs([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x02, 0x03, 0xCD, 0x6B]);
+
+ // Assert - ModbusException
+ }
+
+ #endregion Read Discrete Inputs
+
+ #region Read Holding Registers
+
+ [TestMethod]
+ public void ShouldSerializeReadHoldingRegisters()
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ var bytes = protocol.SerializeReadHoldingRegisters(UNIT_ID, 107, 2);
+
+ // Assert
+ Assert.IsNotNull(bytes);
+ Assert.AreEqual(12, bytes.Count);
+
+ // Transaction id
+ Assert.AreEqual(0x00, bytes[0]);
+ Assert.AreEqual(0x01, bytes[1]);
+
+ // Protocol identifier
+ Assert.AreEqual(0x00, bytes[2]);
+ Assert.AreEqual(0x00, bytes[3]);
+
+ // Following bytes
+ Assert.AreEqual(0x00, bytes[4]);
+ Assert.AreEqual(0x06, bytes[5]);
+
+ // Unit id
+ Assert.AreEqual(UNIT_ID, bytes[6]);
+
+ // Function code
+ Assert.AreEqual(0x03, bytes[7]);
+
+ // Starting address
+ Assert.AreEqual(0x00, bytes[8]);
+ Assert.AreEqual(0x6B, bytes[9]);
+ // Quantity
+ Assert.AreEqual(0x00, bytes[10]);
+ Assert.AreEqual(0x02, bytes[11]);
+ }
+
+ [DataTestMethod]
+ [DataRow(0)]
+ [DataRow(126)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForCountOnSerializeReadHoldingRegisters(int count)
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.SerializeReadHoldingRegisters(UNIT_ID, 19, (ushort)count);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadHoldingRegisters()
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.SerializeReadHoldingRegisters(UNIT_ID, ushort.MaxValue, 2);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ public void ShouldDeserializeReadHoldingRegisters()
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ var registers = protocol.DeserializeReadHoldingRegisters([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x03, 0x04, 0x02, 0x2B, 0x00, 0x64]);
+
+ // Assert
+ Assert.IsNotNull(registers);
+ Assert.AreEqual(2, registers.Count);
+
+ Assert.AreEqual(0, registers[0].Address);
+ Assert.AreEqual(555, registers[0].Value);
+
+ Assert.AreEqual(1, registers[1].Address);
+ Assert.AreEqual(100, registers[1].Value);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowExceptionOnDeserializeReadHoldingRegisters()
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.DeserializeReadHoldingRegisters([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x03, 0x04, 0x02, 0x2B]);
+
+ // Assert - ModbusException
+ }
+
+ #endregion Read Holding Registers
+
+ #region Read Input Registers
+
+ [TestMethod]
+ public void ShouldSerializeReadInputRegisters()
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ var bytes = protocol.SerializeReadInputRegisters(UNIT_ID, 109, 3);
+
+ // Assert
+ Assert.IsNotNull(bytes);
+ Assert.AreEqual(12, bytes.Count);
+
+ // Transaction id
+ Assert.AreEqual(0x00, bytes[0]);
+ Assert.AreEqual(0x01, bytes[1]);
+
+ // Protocol identifier
+ Assert.AreEqual(0x00, bytes[2]);
+ Assert.AreEqual(0x00, bytes[3]);
+
+ // Following bytes
+ Assert.AreEqual(0x00, bytes[4]);
+ Assert.AreEqual(0x06, bytes[5]);
+
+ // Unit id
+ Assert.AreEqual(UNIT_ID, bytes[6]);
+
+ // Function code
+ Assert.AreEqual(0x04, bytes[7]);
+
+ // Starting address
+ Assert.AreEqual(0x00, bytes[8]);
+ Assert.AreEqual(0x6D, bytes[9]);
+ // Quantity
+ Assert.AreEqual(0x00, bytes[10]);
+ Assert.AreEqual(0x03, bytes[11]);
+ }
+
+ [DataTestMethod]
+ [DataRow(0)]
+ [DataRow(126)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForCountOnSerializeReadInputRegisters(int count)
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.SerializeReadInputRegisters(UNIT_ID, 19, (ushort)count);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadInputRegisters()
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.SerializeReadInputRegisters(UNIT_ID, ushort.MaxValue, 2);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ public void ShouldDeserializeReadInputRegisters()
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ var registers = protocol.DeserializeReadInputRegisters([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x04, 0x04, 0x02, 0x2A, 0x00, 0x60]);
+
+ // Assert
+ Assert.IsNotNull(registers);
+ Assert.AreEqual(2, registers.Count);
+
+ Assert.AreEqual(0, registers[0].Address);
+ Assert.AreEqual(554, registers[0].Value);
+
+ Assert.AreEqual(1, registers[1].Address);
+ Assert.AreEqual(96, registers[1].Value);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowExceptionOnDeserializeReadInputRegisters()
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.DeserializeReadInputRegisters([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x04, 0x04, 0x02, 0x2B]);
+
+ // Assert - ModbusException
+ }
+
+ #endregion Read Input Registers
+
+ #region Write Single Coil
+
+ [TestMethod]
+ public void ShouldSerializeWriteSingleCoil()
+ {
+ // Arrange
+ var coil = new Coil { Address = 109, Value = true };
+ var protocol = new TcpProtocol();
+
+ // Act
+ var result = protocol.SerializeWriteSingleCoil(UNIT_ID, coil);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual(12, result.Count);
+
+ // Transaction id
+ Assert.AreEqual(0x00, result[0]);
+ Assert.AreEqual(0x01, result[1]);
+
+ // Protocol identifier
+ Assert.AreEqual(0x00, result[2]);
+ Assert.AreEqual(0x00, result[3]);
+
+ // Following bytes
+ Assert.AreEqual(0x00, result[4]);
+ Assert.AreEqual(0x06, result[5]);
+
+ // Unit id
+ Assert.AreEqual(UNIT_ID, result[6]);
+
+ // Function code
+ Assert.AreEqual(0x05, result[7]);
+
+ // Starting address
+ Assert.AreEqual(0x00, result[8]);
+ Assert.AreEqual(0x6D, result[9]);
+
+ // Value
+ Assert.AreEqual(0xFF, result[10]);
+ Assert.AreEqual(0x00, result[11]);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public void ShouldThrowArgumentNullOnSerializeWriteSingleCoil()
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.SerializeWriteSingleCoil(UNIT_ID, null);
+
+ // Assert - ArgumentNullException
+ }
+
+ [TestMethod]
+ public void ShouldDeserializeWriteSingleCoil()
+ {
+ // Arrange
+ byte[] bytes = [0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x05, 0x01, 0x0A, 0xFF, 0x00];
+ var protocol = new TcpProtocol();
+
+ // Act
+ var coil = protocol.DeserializeWriteSingleCoil(bytes);
+
+ // Assert
+ Assert.IsNotNull(coil);
+ Assert.AreEqual(266, coil.Address);
+ Assert.IsTrue(coil.Value);
+ }
+
+ #endregion Write Single Coil
+
+ #region Write Single Register
+
+ [TestMethod]
+ public void ShouldSerializeWriteSingleHoldingRegister()
+ {
+ // Arrange
+ var register = new HoldingRegister { Address = 109, Value = 123 };
+ var protocol = new TcpProtocol();
+
+ // Act
+ var result = protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, register);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual(12, result.Count);
+
+ // Transaction id
+ Assert.AreEqual(0x00, result[0]);
+ Assert.AreEqual(0x01, result[1]);
+
+ // Protocol identifier
+ Assert.AreEqual(0x00, result[2]);
+ Assert.AreEqual(0x00, result[3]);
+
+ // Following bytes
+ Assert.AreEqual(0x00, result[4]);
+ Assert.AreEqual(0x06, result[5]);
+
+ // Unit id
+ Assert.AreEqual(UNIT_ID, result[6]);
+
+ // Function code
+ Assert.AreEqual(0x06, result[7]);
+
+ // Starting address
+ Assert.AreEqual(0x00, result[8]);
+ Assert.AreEqual(0x6D, result[9]);
+
+ // Value
+ Assert.AreEqual(0x00, result[10]);
+ Assert.AreEqual(0x7B, result[11]);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public void ShouldThrowArgumentNullOnSerializeWriteSingleHoldingRegister()
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, null);
+
+ // Assert - ArgumentNullException
+ }
+
+ [TestMethod]
+ public void ShouldDeserializeWriteSingleHoldingRegister()
+ {
+ // Arrange
+ byte[] bytes = [0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x06, 0x02, 0x02, 0x01, 0x23];
+ var protocol = new TcpProtocol();
+
+ // Act
+ var register = protocol.DeserializeWriteSingleHoldingRegister(bytes);
+
+ // Assert
+ Assert.IsNotNull(register);
+ Assert.AreEqual(514, register.Address);
+ Assert.AreEqual(291, register.Value);
+ }
+
+ #endregion Write Single Register
+
+ #region Write Multiple Coils
+
+ [TestMethod]
+ public void ShouldSerializeWriteMultipleCoils()
+ {
+ // Arrange
+ var coils = new Coil[]
+ {
+ new() { Address = 10, Value = true },
+ new() { Address = 11, Value = false },
+ new() { Address = 12, Value = true },
+ new() { Address = 13, Value = false },
+ new() { Address = 14, Value = true },
+ };
+ var protocol = new TcpProtocol();
+
+ // Act
+ var result = protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual(14, result.Count);
+
+ // Transaction id
+ Assert.AreEqual(0x00, result[0]);
+ Assert.AreEqual(0x01, result[1]);
+
+ // Protocol identifier
+ Assert.AreEqual(0x00, result[2]);
+ Assert.AreEqual(0x00, result[3]);
+
+ // Following bytes
+ Assert.AreEqual(0x00, result[4]);
+ Assert.AreEqual(0x08, result[5]);
+
+ // Unit id
+ Assert.AreEqual(UNIT_ID, result[6]);
+
+ // Function code
+ Assert.AreEqual(0x0F, result[7]);
+
+ // Starting address
+ Assert.AreEqual(0x00, result[8]);
+ Assert.AreEqual(0x0A, result[9]);
+
+ // Quantity
+ Assert.AreEqual(0x00, result[10]);
+ Assert.AreEqual(0x05, result[11]);
+
+ // Byte count
+ Assert.AreEqual(0x01, result[12]);
+
+ // Values
+ Assert.AreEqual(0x15, result[13]);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public void ShouldTrhowArgumentNullOnSerializeWriteMultipleCoils()
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.SerializeWriteMultipleCoils(UNIT_ID, null);
+
+ // Assert - ArgumentNullException
+ }
+
+ [DataTestMethod]
+ [DataRow(0)]
+ [DataRow(1969)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleCoils(int count)
+ {
+ // Arrange
+ var coils = new List();
+ for (int i = 0; i < count; i++)
+ coils.Add(new() { Address = (ushort)i });
+
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleCoils()
+ {
+ // Arrange
+ var coils = new Coil[]
+ {
+ new() { Address = 10, Value = true },
+ new() { Address = 10, Value = false },
+ };
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
+
+ // Assert - ArgumentException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleCoils()
+ {
+ // Arrange
+ var coils = new Coil[]
+ {
+ new() { Address = 10, Value = true },
+ new() { Address = 12, Value = false },
+ };
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
+
+ // Assert - ArgumentException
+ }
+
+ [TestMethod]
+ public void ShouldDeserializeWriteMultipleCoils()
+ {
+ // Arrange
+ byte[] bytes = [0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x0F, 0x01, 0x0A, 0x00, 0x0B];
+ var protocol = new TcpProtocol();
+
+ // Act
+ var (firstAddress, numberOfCoils) = protocol.DeserializeWriteMultipleCoils(bytes);
+
+ // Assert
+ Assert.AreEqual(266, firstAddress);
+ Assert.AreEqual(11, numberOfCoils);
+ }
+
+ #endregion Write Multiple Coils
+
+ #region Write Multiple Holding Registers
+
+ [TestMethod]
+ public void ShouldSerializeWriteMultipleHoldingRegisters()
+ {
+ // Arrange
+ var registers = new HoldingRegister[]
+ {
+ new() { Address = 10, Value = 10 },
+ new() { Address = 11, Value = 11 }
+ };
+ var protocol = new TcpProtocol();
+
+ // Act
+ var result = protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual(17, result.Count);
+
+ // Transaction id
+ Assert.AreEqual(0x00, result[0]);
+ Assert.AreEqual(0x01, result[1]);
+
+ // Protocol identifier
+ Assert.AreEqual(0x00, result[2]);
+ Assert.AreEqual(0x00, result[3]);
+
+ // Following bytes
+ Assert.AreEqual(0x00, result[4]);
+ Assert.AreEqual(0x0B, result[5]);
+
+ // Unit id
+ Assert.AreEqual(UNIT_ID, result[6]);
+
+ // Function code
+ Assert.AreEqual(0x10, result[7]);
+
+ // Starting address
+ Assert.AreEqual(0x00, result[8]);
+ Assert.AreEqual(0x0A, result[9]);
+
+ // Quantity
+ Assert.AreEqual(0x00, result[10]);
+ Assert.AreEqual(0x02, result[11]);
+
+ // Byte count
+ Assert.AreEqual(0x04, result[12]);
+
+ // Values
+ Assert.AreEqual(0x00, result[13]);
+ Assert.AreEqual(0x0A, result[14]);
+ Assert.AreEqual(0x00, result[15]);
+ Assert.AreEqual(0x0B, result[16]);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public void ShouldTrhowArgumentNullOnSerializeWriteMultipleHoldingRegisters()
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, null);
+
+ // Assert - ArgumentNullException
+ }
+
+ [DataTestMethod]
+ [DataRow(0)]
+ [DataRow(124)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleHoldingRegisters(int count)
+ {
+ // Arrange
+ var registers = new List();
+ for (int i = 0; i < count; i++)
+ registers.Add(new() { Address = (ushort)i });
+
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleHoldingRegisters()
+ {
+ // Arrange
+ var registers = new HoldingRegister[]
+ {
+ new() { Address = 10 },
+ new() { Address = 10 },
+ };
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
+
+ // Assert - ArgumentException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleHoldingRegisters()
+ {
+ // Arrange
+ var registers = new HoldingRegister[]
+ {
+ new() { Address = 10 },
+ new() { Address = 12 },
+ };
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
+
+ // Assert - ArgumentException
+ }
+
+ [TestMethod]
+ public void ShouldDeserializeWriteMultipleHoldingRegisters()
+ {
+ // Arrange
+ byte[] bytes = [0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x10, 0x02, 0x0A, 0x00, 0x0A];
+ var protocol = new TcpProtocol();
+
+ // Act
+ var (firstAddress, numberOfCoils) = protocol.DeserializeWriteMultipleHoldingRegisters(bytes);
+
+ // Assert
+ Assert.AreEqual(522, firstAddress);
+ Assert.AreEqual(10, numberOfCoils);
+ }
+
+ #endregion Write Multiple Holding Registers
+
+ #region Validation
+
+ [TestMethod]
+ public void ShouldReturnFalseForMinLengthOnCheckResponseComplete()
+ {
+ // Arrange
+ byte[] bytes = [0x00, 0x01, 0x00];
+ var protocol = new TcpProtocol();
+
+ // Act
+ bool complete = protocol.CheckResponseComplete(bytes);
+
+ // Assert
+ Assert.IsFalse(complete);
+ }
+
+ [TestMethod]
+ public void ShouldReturnFalseForFollowingBytesOnCheckResponseComplete()
+ {
+ // Arrange
+ byte[] bytes = [0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x81];
+ var protocol = new TcpProtocol();
+
+ // Act
+ bool complete = protocol.CheckResponseComplete(bytes);
+
+ // Assert
+ Assert.IsFalse(complete);
+ }
+
+ [TestMethod]
+ public void ShouldReturnTrueOnCheckResponseComplete()
+ {
+ // Arrange
+ byte[] bytes = [0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x81, 0x01];
+ var protocol = new TcpProtocol();
+
+ // Act
+ bool complete = protocol.CheckResponseComplete(bytes);
+
+ // Assert
+ Assert.IsTrue(complete);
+ }
+
+ [TestMethod]
+ public void ShouldValidateResponse()
+ {
+ // Arrange
+ byte[] request = [0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x01, 0x00, 0x01, 0x00, 0x02];
+ byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x2A, 0x01, 0x01, 0x00];
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.ValidateResponse(request, response);
+ }
+
+ [TestMethod]
+ public void ShouldValidateResponseIgnoringTransactionId()
+ {
+ // Arrange
+ byte[] request = [0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x01, 0x00, 0x01, 0x00, 0x02];
+ byte[] response = [0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x2A, 0x01, 0x01, 0x00];
+ var protocol = new TcpProtocol { DisableTransactionId = true };
+
+ // Act
+ protocol.ValidateResponse(request, response);
+ }
+
+ [DataTestMethod]
+ [DataRow(0x00, 0x00)]
+ [DataRow(0x01, 0x01)]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowForTransactionIdOnValidateResponse(int hi, int lo)
+ {
+ // Arrange
+ byte[] request = [(byte)hi, (byte)lo, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x01, 0x00, 0x01, 0x00, 0x02];
+ byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x2A, 0x01, 0x01, 0x00];
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.ValidateResponse(request, response);
+ }
+
+ [DataTestMethod]
+ [DataRow(0x00, 0x01)]
+ [DataRow(0x01, 0x00)]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowForProtocolIdOnValidateResponse(int hi, int lo)
+ {
+ // Arrange
+ byte[] request = [0x00, 0x01, (byte)hi, (byte)lo, 0x00, 0x06, 0x2A, 0x01, 0x00, 0x01, 0x00, 0x02];
+ byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x2A, 0x01, 0x01, 0x00];
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.ValidateResponse(request, response);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowForFollowingBytesOnValidateResponse()
+ {
+ // Arrange
+ byte[] request = [0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x01, 0x00, 0x01, 0x00, 0x02];
+ byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x2A, 0x01, 0x01, 0x00];
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.ValidateResponse(request, response);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowForUnitIdOnValidateResponse()
+ {
+ // Arrange
+ byte[] request = [0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x01, 0x00, 0x01, 0x00, 0x02];
+ byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x2B, 0x01, 0x01, 0x00];
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.ValidateResponse(request, response);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowForFunctionCodeOnValidateResponse()
+ {
+ // Arrange
+ byte[] request = [0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x01, 0x00, 0x01, 0x00, 0x02];
+ byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x2A, 0x02, 0x01, 0x00];
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.ValidateResponse(request, response);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ModbusException))]
+ public void ShouldThrowForModbusErrorOnValidateResponse()
+ {
+ // Arrange
+ byte[] request = [0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x01, 0x00, 0x01, 0x00, 0x02];
+ byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x2A, 0x81, 0x01];
+ var protocol = new TcpProtocol();
+
+ // Act
+ protocol.ValidateResponse(request, response);
+ }
+
+ #endregion Validation
+
+ #region Helper
+
+ [TestMethod]
+ public void ShouldIncreaseTransactionId()
+ {
+ // Arrange
+ var list = new List();
+ var protocol = new TcpProtocol();
+
+ // Act
+ list.Add(protocol.SerializeReadCoils(UNIT_ID, 10, 10).ToArray());
+ list.Add(protocol.SerializeReadCoils(UNIT_ID, 10, 10).ToArray());
+ list.Add(protocol.SerializeReadCoils(UNIT_ID, 10, 10).ToArray());
+
+ // Assert
+ for (int i = 0; i < list.Count; i++)
+ {
+ Assert.AreEqual(0x00, list[i][0]);
+ Assert.AreEqual((byte)(i + 1), list[i][1]);
+
+ // Other asserts already done
+ }
+ }
+
+ [TestMethod]
+ public void ShouldNotIncreaseTransactionId()
+ {
+ // Arrange
+ var list = new List();
+ var protocol = new TcpProtocol { DisableTransactionId = true };
+
+ // Act
+ list.Add(protocol.SerializeReadCoils(UNIT_ID, 10, 10).ToArray());
+ list.Add(protocol.SerializeReadCoils(UNIT_ID, 10, 10).ToArray());
+ list.Add(protocol.SerializeReadCoils(UNIT_ID, 10, 10).ToArray());
+
+ // Assert
+ for (int i = 0; i < list.Count; i++)
+ {
+ Assert.AreEqual(0x00, list[i][0]);
+ Assert.AreEqual(0x00, list[i][1]);
+
+ // Other asserts already done
+ }
+ }
+
+ [TestMethod]
+ public void ShouldResetTransactionIdOnMaxValue()
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+ protocol.GetType()
+ .GetField("_transactionId", BindingFlags.NonPublic | BindingFlags.Instance)
+ .SetValue(protocol, (ushort)(ushort.MaxValue - 1));
+
+ // Act
+ var result1 = protocol.SerializeReadCoils(UNIT_ID, 10, 10);
+ var result2 = protocol.SerializeReadCoils(UNIT_ID, 10, 10);
+
+ // Assert
+ Assert.AreEqual(0xFF, result1[0]);
+ Assert.AreEqual(0xFF, result1[1]);
+
+ Assert.AreEqual(0x00, result2[0]);
+ Assert.AreEqual(0x00, result2[1]);
+ }
+
+ [TestMethod]
+ public void ShouldNameTcp()
+ {
+ // Arrange
+ var protocol = new TcpProtocol();
+
+ // Act
+ var result = protocol.Name;
+
+ // Assert
+ Assert.AreEqual("TCP", result);
+ }
+
+ #endregion Helper
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Tests/GlobalUsings.cs b/AMWD.Protocols.Modbus.Tests/GlobalUsings.cs
new file mode 100644
index 0000000..381ca26
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Tests/GlobalUsings.cs
@@ -0,0 +1,4 @@
+global using AMWD.Protocols.Modbus.Common;
+global using Microsoft.VisualStudio.TestTools.UnitTesting;
+global using System;
+global using System.Linq;
diff --git a/AMWD.Protocols.Modbus.sln b/AMWD.Protocols.Modbus.sln
new file mode 100644
index 0000000..749066f
--- /dev/null
+++ b/AMWD.Protocols.Modbus.sln
@@ -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
diff --git a/README.md b/README.md
index 1b45399..1319739 100644
--- a/README.md
+++ b/README.md
@@ -2,36 +2,45 @@
Here you can find a basic implementation of the Modbus protocol.
-## Package Overview
+## Overview
The project is divided into three parts.
To be mentioned at the beginning:
-Only the clients are build very modular to fit any requirement reached on the first implementation back in 2018 ([see here](https://github.com/andreasAmMueller/Modbus)).
+Only the clients are build very modular to fit any requirement reached on the first implementation back in 2018 ([see here]).
The server implementations will only cover their defaults!
-
-### Common
+### [Common]
Here you'll find all the common interfaces and base implementations for Modbus.
For example the default protocol versions: `TCP`, `RTU` and `ASCII`.
+With this package you'll have anything you need to create your own client implementations.
-### Serial
+### [Serial]
-Here you'll find all the serial protocol implementations.
+This package contains some wrappers and implementations for the serial protocol.
+So you can use it out of the box to communicate via serial line ports / devices.
+### [TCP]
-### TCP
-
-Here you'll find all the TCP protocol implementations.
-
+This package contains the default implementations for network communication via TCP.
+It uses a specific TCP connection implementation and plugs all things from the Common package together.
---
-Published under [MIT License](LICENSE.txt) (see [**tl;dr**Legal](https://www.tldrlegal.com/license/mit-license))
+Published under [MIT License] (see [**tl;dr**Legal])
+
+
+
+[see here]: https://github.com/andreasAMmueller/Modbus
+[Common]: AMWD.Protocols.Modbus.Common/README.md
+[Serial]: AMWD.Protocols.Modbus.Serial/README.md
+[TCP]: AMWD.Protocols.Modbus.Tcp/README.md
+[MIT License]: LICENSE.txt
+[**tl;dr**Legal]: https://www.tldrlegal.com/license/mit-license