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