From 05759f8e12b952837879e5100cd8b4cd9e3ccd0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Mon, 27 Jan 2025 17:26:56 +0100 Subject: [PATCH] Fixes for SerialRtuProxy - Adding UnitTests - Fixing some bugs - Updating UnitTest dependencies --- .gitlab-ci.yml | 42 +- .../Utils/VirtualModbusClient.cs | 3 +- .../ModbusRtuProxy.cs | 75 +- .../ModbusRtuServer.cs | 1185 +--------- .../Utils/SerialPortWrapper.cs | 15 +- .../AMWD.Protocols.Modbus.Tests.csproj | 11 +- AMWD.Protocols.Modbus.Tests/Helper.cs | 23 + .../Serial/ModbusRtuProxyTest.cs | 2006 +++++++++++++++++ .../ShouldReadDeviceIdentification.snap.bin | 9 + 9 files changed, 2163 insertions(+), 1206 deletions(-) create mode 100644 AMWD.Protocols.Modbus.Tests/Helper.cs create mode 100644 AMWD.Protocols.Modbus.Tests/Serial/ModbusRtuProxyTest.cs create mode 100644 AMWD.Protocols.Modbus.Tests/Serial/Snapshots/ModbusRtuProxyTest/ShouldReadDeviceIdentification.snap.bin diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 85c1f8b..3d36d5c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,10 +20,10 @@ build-debug: rules: - if: $CI_COMMIT_TAG == null script: - - dotnet restore --no-cache --force - - dotnet build -c Debug --nologo --no-restore --no-incremental - shopt -s globstar - mkdir ./artifacts + - dotnet restore --no-cache --force + - dotnet build -c Debug --nologo --no-restore --no-incremental - mv ./**/*.nupkg ./artifacts/ - mv ./**/*.snupkg ./artifacts/ artifacts: @@ -42,10 +42,20 @@ test-debug: - 64bit rules: - if: $CI_COMMIT_TAG == null - coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/' + coverage: /Branch coverage[\s\S].+%/ + before_script: + - dotnet tool install dotnet-reportgenerator-globaltool --tool-path /dotnet-tools script: - - dotnet restore --no-cache --force - - dotnet test -c Debug --nologo --no-restore + - dotnet test -c Debug --nologo /p:CoverletOutputFormat=Cobertura + - /dotnet-tools/reportgenerator "-reports:${CI_PROJECT_DIR}/**/coverage.cobertura.xml" "-targetdir:/reports" -reportType:TextSummary + after_script: + - cat /reports/Summary.txt + artifacts: + when: always + reports: + coverage_report: + coverage_format: cobertura + path: ./**/coverage.cobertura.xml deploy-debug: stage: deploy @@ -72,17 +82,17 @@ build-release: rules: - if: $CI_COMMIT_TAG != null script: - - dotnet restore --no-cache --force - - dotnet build -c Release --nologo --no-restore --no-incremental - shopt -s globstar - mkdir ./artifacts + - dotnet restore --no-cache --force + - dotnet build -c Release --nologo --no-restore --no-incremental - mv ./**/*.nupkg ./artifacts/ - mv ./**/*.snupkg ./artifacts/ artifacts: paths: - artifacts/*.nupkg - artifacts/*.snupkg - expire_in: 1 days + expire_in: 7 days test-release: stage: test @@ -94,10 +104,20 @@ test-release: - amd64 rules: - if: $CI_COMMIT_TAG != null - coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/' + coverage: /Branch coverage[\s\S].+%/ + before_script: + - dotnet tool install dotnet-reportgenerator-globaltool --tool-path /dotnet-tools script: - - dotnet restore --no-cache --force - - dotnet test -c Release --nologo --no-restore + - dotnet test -c Release --nologo /p:CoverletOutputFormat=Cobertura + - /dotnet-tools/reportgenerator "-reports:${CI_PROJECT_DIR}/**/coverage.cobertura.xml" "-targetdir:/reports" -reportType:TextSummary + after_script: + - cat /reports/Summary.txt + artifacts: + when: always + reports: + coverage_report: + coverage_format: cobertura + path: ./**/coverage.cobertura.xml deploy-release: stage: deploy diff --git a/AMWD.Protocols.Modbus.Common/Utils/VirtualModbusClient.cs b/AMWD.Protocols.Modbus.Common/Utils/VirtualModbusClient.cs index 26dd52f..3e8e420 100644 --- a/AMWD.Protocols.Modbus.Common/Utils/VirtualModbusClient.cs +++ b/AMWD.Protocols.Modbus.Common/Utils/VirtualModbusClient.cs @@ -48,7 +48,8 @@ namespace AMWD.Protocols.Modbus.Common.Utils #region Properties - internal VirtualProtocol TypedProtocol => Protocol as VirtualProtocol; + internal VirtualProtocol TypedProtocol + => Protocol as VirtualProtocol; #endregion Properties diff --git a/AMWD.Protocols.Modbus.Serial/ModbusRtuProxy.cs b/AMWD.Protocols.Modbus.Serial/ModbusRtuProxy.cs index 7c62700..3670006 100644 --- a/AMWD.Protocols.Modbus.Serial/ModbusRtuProxy.cs +++ b/AMWD.Protocols.Modbus.Serial/ModbusRtuProxy.cs @@ -618,7 +618,7 @@ namespace AMWD.Protocols.Modbus.Serial return null; var responseBytes = new List(); - responseBytes.AddRange(requestBytes.Take(8)); + responseBytes.AddRange(requestBytes.Take(2)); ushort firstAddress = requestBytes.GetBigEndianUInt16(2); ushort count = requestBytes.GetBigEndianUInt16(4); @@ -646,18 +646,18 @@ namespace AMWD.Protocols.Modbus.Serial HighByte = requestBytes[baseOffset + i * 2], LowByte = requestBytes[baseOffset + i * 2 + 1] }); + } - bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[0], list, cancellationToken); - if (isSuccess) - { - // Response is an echo of the request - responseBytes.AddRange(requestBytes.Skip(2).Take(4)); - } - else - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); - } + bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[0], list, cancellationToken); + if (isSuccess) + { + // Response is an echo of the request + responseBytes.AddRange(requestBytes.Skip(2).Take(4)); + } + else + { + responseBytes[1] |= 0x80; + responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); } } catch @@ -671,6 +671,9 @@ namespace AMWD.Protocols.Modbus.Serial private async Task HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken) { + if (requestBytes.Length < 7) + return null; + var responseBytes = new List(); responseBytes.AddRange(requestBytes.Take(2)); @@ -702,7 +705,7 @@ namespace AMWD.Protocols.Modbus.Serial try { - var res = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken); + var deviceInfo = await Client.ReadDeviceIdentificationAsync(requestBytes[0], category, firstObject, cancellationToken); var bodyBytes = new List(); @@ -711,31 +714,20 @@ namespace AMWD.Protocols.Modbus.Serial // Conformity bodyBytes.Add((byte)category); - if (res.IsIndividualAccessAllowed) + if (deviceInfo.IsIndividualAccessAllowed) bodyBytes[2] |= 0x80; // More, NextId, NumberOfObjects bodyBytes.AddRange(new byte[3]); - int maxObjectId; - switch (category) + int maxObjectId = category switch { - case ModbusDeviceIdentificationCategory.Basic: - maxObjectId = 0x02; - break; - - case ModbusDeviceIdentificationCategory.Regular: - maxObjectId = 0x06; - break; - - case ModbusDeviceIdentificationCategory.Extended: - maxObjectId = 0xFF; - break; - - default: // Individual - maxObjectId = requestBytes[4]; - break; - } + ModbusDeviceIdentificationCategory.Basic => 0x02, + ModbusDeviceIdentificationCategory.Regular => 0x06, + ModbusDeviceIdentificationCategory.Extended => 0xFF, + // Individual + _ => requestBytes[4], + }; byte numberOfObjects = 0; for (int i = requestBytes[4]; i <= maxObjectId; i++) @@ -744,7 +736,7 @@ namespace AMWD.Protocols.Modbus.Serial if (0x07 <= i && i <= 0x7F) continue; - byte[] objBytes = GetDeviceObject((byte)i, res); + byte[] objBytes = GetDeviceObject((byte)i, deviceInfo); // We need to split the response if it would exceed the max ADU size if (responseBytes.Count + bodyBytes.Count + objBytes.Length > RtuProtocol.MAX_ADU_LENGTH) @@ -754,7 +746,8 @@ namespace AMWD.Protocols.Modbus.Serial bodyBytes[5] = numberOfObjects; responseBytes.AddRange(bodyBytes); - return [.. responseBytes]; + + return ReturnResponse(responseBytes); } bodyBytes.AddRange(objBytes); @@ -782,7 +775,7 @@ namespace AMWD.Protocols.Modbus.Serial { case ModbusDeviceIdentificationObject.VendorName: { - byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName); + byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName ?? ""); result.Add((byte)bytes.Length); result.AddRange(bytes); } @@ -790,7 +783,7 @@ namespace AMWD.Protocols.Modbus.Serial case ModbusDeviceIdentificationObject.ProductCode: { - byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode); + byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode ?? ""); result.Add((byte)bytes.Length); result.AddRange(bytes); } @@ -798,7 +791,7 @@ namespace AMWD.Protocols.Modbus.Serial case ModbusDeviceIdentificationObject.MajorMinorRevision: { - byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision); + byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision ?? ""); result.Add((byte)bytes.Length); result.AddRange(bytes); } @@ -806,7 +799,7 @@ namespace AMWD.Protocols.Modbus.Serial case ModbusDeviceIdentificationObject.VendorUrl: { - byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl); + byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl ?? ""); result.Add((byte)bytes.Length); result.AddRange(bytes); } @@ -814,7 +807,7 @@ namespace AMWD.Protocols.Modbus.Serial case ModbusDeviceIdentificationObject.ProductName: { - byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName); + byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName ?? ""); result.Add((byte)bytes.Length); result.AddRange(bytes); } @@ -822,7 +815,7 @@ namespace AMWD.Protocols.Modbus.Serial case ModbusDeviceIdentificationObject.ModelName: { - byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName); + byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName ?? ""); result.Add((byte)bytes.Length); result.AddRange(bytes); } @@ -830,7 +823,7 @@ namespace AMWD.Protocols.Modbus.Serial case ModbusDeviceIdentificationObject.UserApplicationName: { - byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName); + byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName ?? ""); result.Add((byte)bytes.Length); result.AddRange(bytes); } diff --git a/AMWD.Protocols.Modbus.Serial/ModbusRtuServer.cs b/AMWD.Protocols.Modbus.Serial/ModbusRtuServer.cs index 4441aa9..3c8210b 100644 --- a/AMWD.Protocols.Modbus.Serial/ModbusRtuServer.cs +++ b/AMWD.Protocols.Modbus.Serial/ModbusRtuServer.cs @@ -1,79 +1,36 @@ using System; -using System.Collections.Generic; -using System.IO.Ports; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading; -using System.Threading.Tasks; using AMWD.Protocols.Modbus.Common; using AMWD.Protocols.Modbus.Common.Events; -using AMWD.Protocols.Modbus.Common.Models; -using AMWD.Protocols.Modbus.Common.Protocols; +using AMWD.Protocols.Modbus.Common.Utils; namespace AMWD.Protocols.Modbus.Serial { /// - /// A basic implementation of a Modbus serial line RTU server. + /// Implements a Modbus serial line RTU server proxying all requests to a virtual Modbus client. /// [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public class ModbusRtuServer : IDisposable + public class ModbusRtuServer : ModbusRtuProxy { - #region Fields - - private bool _isDisposed; - - private SerialPort _serialPort; - private CancellationTokenSource _stopCts; - - private readonly ReaderWriterLockSlim _deviceListLock = new(); - private readonly Dictionary _devices = []; - - #endregion Fields - - #region Constructors - /// /// Initializes a new instance of the class. /// /// The name of the serial port to use. - /// The baud rate of the serial port (Default: 19.200). - public ModbusRtuServer(string portName, BaudRate baudRate = BaudRate.Baud19200) + public ModbusRtuServer(string portName) + : base(new VirtualModbusClient(), portName) { - if (string.IsNullOrWhiteSpace(portName)) - throw new ArgumentNullException(nameof(portName)); - - if (!Enum.IsDefined(typeof(BaudRate), baudRate)) - throw new ArgumentOutOfRangeException(nameof(baudRate)); - - if (!ModbusSerialClient.AvailablePortNames.Contains(portName)) - throw new ArgumentException($"The serial port ({portName}) is not available.", nameof(portName)); - - _serialPort = new SerialPort - { - PortName = portName, - BaudRate = (int)baudRate, - Handshake = Handshake.None, - DataBits = 8, - ReadTimeout = 1000, - RtsEnable = false, - StopBits = StopBits.One, - WriteTimeout = 1000, - Parity = Parity.Even - }; + TypedClient.CoilWritten += (sender, e) => CoilWritten?.Invoke(this, e); + TypedClient.RegisterWritten += (sender, e) => RegisterWritten?.Invoke(this, e); } - #endregion Constructors - #region Events /// - /// Occurs when a is written. + /// Indicates that a -value received through a remote client has been written. /// public event EventHandler CoilWritten; /// - /// Occurs when a is written. + /// Indicates that a -value received from a remote client has been written. /// public event EventHandler RegisterWritten; @@ -81,1097 +38,57 @@ namespace AMWD.Protocols.Modbus.Serial #region Properties - /// - public string PortName => _serialPort.PortName; - - /// - /// Gets or sets the baud rate of the serial port. - /// - public BaudRate BaudRate - { - get => (BaudRate)_serialPort.BaudRate; - set => _serialPort.BaudRate = (int)value; - } - - /// - public Handshake Handshake - { - get => _serialPort.Handshake; - set => _serialPort.Handshake = value; - } - - /// - public int DataBits - { - get => _serialPort.DataBits; - set => _serialPort.DataBits = value; - } - - /// - public bool IsOpen => _serialPort.IsOpen; - - /// - /// Gets or sets the before a time-out occurs when a read operation does not finish. - /// - public TimeSpan ReadTimeout - { - get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout); - set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds; - } - - /// - public bool RtsEnable - { - get => _serialPort.RtsEnable; - set => _serialPort.RtsEnable = value; - } - - /// - public StopBits StopBits - { - get => _serialPort.StopBits; - set => _serialPort.StopBits = value; - } - - /// - /// Gets or sets the before a time-out occurs when a write operation does not finish. - /// - public TimeSpan WriteTimeout - { - get => TimeSpan.FromMilliseconds(_serialPort.WriteTimeout); - set => _serialPort.WriteTimeout = (int)value.TotalMilliseconds; - } - - /// - public Parity Parity - { - get => _serialPort.Parity; - set => _serialPort.Parity = value; - } + internal VirtualModbusClient TypedClient + => Client as VirtualModbusClient; #endregion Properties - #region Control Methods - - /// - /// Starts the server. - /// - /// A cancellation token used to propagate notification that this operation should be canceled. - public Task StartAsync(CancellationToken cancellationToken = default) - { - Assertions(); - - _stopCts?.Cancel(); - _serialPort.Close(); - _serialPort.DataReceived -= OnDataReceived; - - _stopCts?.Dispose(); - _stopCts = new CancellationTokenSource(); - - _serialPort.DataReceived += OnDataReceived; - _serialPort.Open(); - - return Task.CompletedTask; - } - - /// - /// Stops the server. - /// - /// A cancellation token used to propagate notification that this operation should be canceled. - public Task StopAsync(CancellationToken cancellationToken = default) - { - Assertions(); - return StopAsyncInternal(cancellationToken); - } - - private Task StopAsyncInternal(CancellationToken cancellationToken) - { - _stopCts?.Cancel(); - - _serialPort.Close(); - _serialPort.DataReceived -= OnDataReceived; - - return Task.CompletedTask; - } - - /// - /// Releases all managed and unmanaged resources used by the . - /// - public void Dispose() - { - if (_isDisposed) - return; - - _isDisposed = true; - - StopAsyncInternal(CancellationToken.None).Wait(); - - _deviceListLock.Dispose(); - _devices.Clear(); - - _serialPort.Dispose(); - _stopCts?.Dispose(); - } - - private void Assertions() - { -#if NET8_0_OR_GREATER - ObjectDisposedException.ThrowIf(_isDisposed, this); -#else - if (_isDisposed) - throw new ObjectDisposedException(GetType().FullName); -#endif - } - - #endregion Control Methods - - #region Client Handling - - private void OnDataReceived(object _, SerialDataReceivedEventArgs evArgs) - { - try - { - var requestBytes = new List(); - do - { - byte[] buffer = new byte[RtuProtocol.MAX_ADU_LENGTH]; - int count = _serialPort.Read(buffer, 0, buffer.Length); - requestBytes.AddRange(buffer.Take(count)); - - _stopCts.Token.ThrowIfCancellationRequested(); - } - while (_serialPort.BytesToRead > 0); - - _stopCts.Token.ThrowIfCancellationRequested(); - byte[] responseBytes = HandleRequest(requestBytes.ToArray()); - if (responseBytes == null) - return; - - _stopCts.Token.ThrowIfCancellationRequested(); - _serialPort.Write(responseBytes, 0, responseBytes.Length); - } - catch - { /* keep it quiet */ } - } - - #endregion Client Handling - - #region Request Handling - - private byte[] HandleRequest(byte[] requestBytes) - { - byte[] recvCrc = requestBytes.Skip(requestBytes.Length - 2).ToArray(); - byte[] calcCrc = RtuProtocol.CRC16(requestBytes, 0, requestBytes.Length - 2); - if (!recvCrc.SequenceEqual(calcCrc)) - return null; - - using (_deviceListLock.GetReadLock()) - { - // No response is sent, if the device is not known - if (!_devices.TryGetValue(requestBytes[0], out var device)) - return null; - - switch ((ModbusFunctionCode)requestBytes[1]) - { - case ModbusFunctionCode.ReadCoils: - return HandleReadCoils(device, requestBytes); - - case ModbusFunctionCode.ReadDiscreteInputs: - return HandleReadDiscreteInputs(device, requestBytes); - - case ModbusFunctionCode.ReadHoldingRegisters: - return HandleReadHoldingRegisters(device, requestBytes); - - case ModbusFunctionCode.ReadInputRegisters: - return HandleReadInputRegisters(device, requestBytes); - - case ModbusFunctionCode.WriteSingleCoil: - return HandleWriteSingleCoil(device, requestBytes); - - case ModbusFunctionCode.WriteSingleRegister: - return HandleWriteSingleRegister(device, requestBytes); - - case ModbusFunctionCode.WriteMultipleCoils: - return HandleWriteMultipleCoils(device, requestBytes); - - case ModbusFunctionCode.WriteMultipleRegisters: - return HandleWriteMultipleRegisters(device, requestBytes); - - case ModbusFunctionCode.EncapsulatedInterface: - return HandleEncapsulatedInterface(requestBytes); - - default: // unknown function - { - byte[] responseBytes = new byte[5]; - Array.Copy(requestBytes, 0, responseBytes, 0, 2); - - // Mark as error - responseBytes[1] |= 0x80; - - responseBytes[2] = (byte)ModbusErrorCode.IllegalFunction; - - SetCrc(responseBytes); - return responseBytes; - } - } - } - } - - private static byte[] HandleReadCoils(ModbusDevice device, byte[] requestBytes) - { - if (requestBytes.Length < 8) - return null; - - var responseBytes = new List(); - responseBytes.AddRange(requestBytes.Take(2)); - - ushort firstAddress = requestBytes.GetBigEndianUInt16(2); - ushort count = requestBytes.GetBigEndianUInt16(4); - - if (count < RtuProtocol.MIN_READ_COUNT || RtuProtocol.MAX_DISCRETE_READ_COUNT < count) - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - if (firstAddress + count > ushort.MaxValue) - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress); - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - try - { - byte[] values = new byte[(int)Math.Ceiling(count / 8.0)]; - for (int i = 0; i < count; i++) - { - ushort address = (ushort)(firstAddress + i); - if (device.GetCoil(address).Value) - { - int byteIndex = i / 8; - int bitIndex = i % 8; - - values[byteIndex] |= (byte)(1 << bitIndex); - } - } - - responseBytes.Add((byte)values.Length); - responseBytes.AddRange(values); - } - catch - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); - } - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - private static byte[] HandleReadDiscreteInputs(ModbusDevice device, byte[] requestBytes) - { - if (requestBytes.Length < 8) - return null; - - var responseBytes = new List(); - responseBytes.AddRange(requestBytes.Take(2)); - - ushort firstAddress = requestBytes.GetBigEndianUInt16(2); - ushort count = requestBytes.GetBigEndianUInt16(4); - - if (count < RtuProtocol.MIN_READ_COUNT || RtuProtocol.MAX_DISCRETE_READ_COUNT < count) - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - if (firstAddress + count > ushort.MaxValue) - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress); - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - try - { - byte[] values = new byte[(int)Math.Ceiling(count / 8.0)]; - for (int i = 0; i < count; i++) - { - ushort address = (ushort)(firstAddress + i); - if (device.GetDiscreteInput(address).Value) - { - int byteIndex = i / 8; - int bitIndex = i % 8; - - values[byteIndex] |= (byte)(1 << bitIndex); - } - } - - responseBytes.Add((byte)values.Length); - responseBytes.AddRange(values); - } - catch - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); - } - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - private static byte[] HandleReadHoldingRegisters(ModbusDevice device, byte[] requestBytes) - { - if (requestBytes.Length < 8) - return null; - - var responseBytes = new List(); - responseBytes.AddRange(requestBytes.Take(2)); - - ushort firstAddress = requestBytes.GetBigEndianUInt16(2); - ushort count = requestBytes.GetBigEndianUInt16(4); - - if (count < RtuProtocol.MIN_READ_COUNT || RtuProtocol.MAX_REGISTER_READ_COUNT < count) - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - if (firstAddress + count > ushort.MaxValue) - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress); - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - try - { - byte[] values = new byte[count * 2]; - for (int i = 0; i < count; i++) - { - ushort address = (ushort)(firstAddress + i); - var register = device.GetHoldingRegister(address); - - values[i * 2] = register.HighByte; - values[i * 2 + 1] = register.LowByte; - } - - responseBytes.Add((byte)values.Length); - responseBytes.AddRange(values); - } - catch - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); - } - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - private static byte[] HandleReadInputRegisters(ModbusDevice device, byte[] requestBytes) - { - if (requestBytes.Length < 8) - return null; - - var responseBytes = new List(); - responseBytes.AddRange(requestBytes.Take(2)); - - ushort firstAddress = requestBytes.GetBigEndianUInt16(2); - ushort count = requestBytes.GetBigEndianUInt16(4); - - if (count < RtuProtocol.MIN_READ_COUNT || RtuProtocol.MAX_REGISTER_READ_COUNT < count) - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - if (firstAddress + count > ushort.MaxValue) - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress); - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - try - { - byte[] values = new byte[count * 2]; - for (int i = 0; i < count; i++) - { - ushort address = (ushort)(firstAddress + i); - var register = device.GetInputRegister(address); - - values[i * 2] = register.HighByte; - values[i * 2 + 1] = register.LowByte; - } - - responseBytes.Add((byte)values.Length); - responseBytes.AddRange(values); - } - catch - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); - } - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - private byte[] HandleWriteSingleCoil(ModbusDevice device, byte[] requestBytes) - { - if (requestBytes.Length < 8) - return null; - - var responseBytes = new List(); - responseBytes.AddRange(requestBytes.Take(2)); - - ushort address = requestBytes.GetBigEndianUInt16(2); - - if (requestBytes[4] != 0x00 && requestBytes[4] != 0xFF) - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - try - { - device.SetCoil(new Coil - { - Address = address, - HighByte = requestBytes[4] - }); - - // Response is an echo of the request - responseBytes.AddRange(requestBytes.Skip(2).Take(4)); - - // Notify that the coil was written - Task.Run(() => - { - try - { - CoilWritten?.Invoke(this, new CoilWrittenEventArgs - { - UnitId = device.Id, - Address = address, - Value = requestBytes[10] == 0xFF - }); - } - catch - { - // keep everything quiet - } - }); - } - catch - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); - } - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - private byte[] HandleWriteSingleRegister(ModbusDevice device, byte[] requestBytes) - { - if (requestBytes.Length < 8) - return null; - - var responseBytes = new List(); - responseBytes.AddRange(requestBytes.Take(2)); - - ushort address = requestBytes.GetBigEndianUInt16(2); - ushort value = requestBytes.GetBigEndianUInt16(4); - - try - { - device.SetHoldingRegister(new HoldingRegister - { - Address = address, - HighByte = requestBytes[4], - LowByte = requestBytes[5] - }); - - // Response is an echo of the request - responseBytes.AddRange(requestBytes.Skip(2).Take(4)); - - // Notify that the register was written - Task.Run(() => - { - try - { - RegisterWritten?.Invoke(this, new RegisterWrittenEventArgs - { - UnitId = device.Id, - Address = address, - Value = value, - HighByte = requestBytes[10], - LowByte = requestBytes[11] - }); - } - catch - { - // keep everything quiet - } - }); - } - catch - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); - } - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - private byte[] HandleWriteMultipleCoils(ModbusDevice device, byte[] requestBytes) - { - if (requestBytes.Length < 9) - return null; - - var responseBytes = new List(); - responseBytes.AddRange(requestBytes.Take(2)); - - ushort firstAddress = requestBytes.GetBigEndianUInt16(2); - ushort count = requestBytes.GetBigEndianUInt16(4); - - int byteCount = (int)Math.Ceiling(count / 8.0); - if (requestBytes.Length < 9 + byteCount) - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - try - { - int baseOffset = 7; - for (int i = 0; i < count; i++) - { - int bytePosition = i / 8; - int bitPosition = i % 8; - - ushort address = (ushort)(firstAddress + i); - bool value = (requestBytes[baseOffset + bytePosition] & (1 << bitPosition)) > 0; - - device.SetCoil(new Coil - { - Address = address, - HighByte = value ? (byte)0xFF : (byte)0x00 - }); - - // Notify that the coil was written - Task.Run(() => - { - try - { - CoilWritten?.Invoke(this, new CoilWrittenEventArgs - { - UnitId = device.Id, - Address = address, - Value = value - }); - } - catch - { - // keep everything quiet - } - }); - } - - responseBytes.AddRange(requestBytes.Skip(2).Take(4)); - } - catch - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); - } - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - private byte[] HandleWriteMultipleRegisters(ModbusDevice device, byte[] requestBytes) - { - if (requestBytes.Length < 9) - return null; - - var responseBytes = new List(); - responseBytes.AddRange(requestBytes.Take(8)); - - ushort firstAddress = requestBytes.GetBigEndianUInt16(2); - ushort count = requestBytes.GetBigEndianUInt16(4); - - int byteCount = count * 2; - if (requestBytes.Length < 9 + byteCount) - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - try - { - int baseOffset = 7; - for (int i = 0; i < count; i++) - { - ushort address = (ushort)(firstAddress + i); - - device.SetHoldingRegister(new HoldingRegister - { - Address = address, - HighByte = requestBytes[baseOffset + i * 2], - LowByte = requestBytes[baseOffset + i * 2 + 1] - }); - - // Notify that the coil was written - Task.Run(() => - { - try - { - RegisterWritten?.Invoke(this, new RegisterWrittenEventArgs - { - UnitId = device.Id, - Address = address, - Value = requestBytes.GetBigEndianUInt16(baseOffset + i * 2), - HighByte = requestBytes[baseOffset + i * 2], - LowByte = requestBytes[baseOffset + i * 2 + 1] - }); - } - catch - { - // keep everything quiet - } - }); - } - - responseBytes.AddRange(requestBytes.Skip(2).Take(4)); - } - catch - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); - } - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - private byte[] HandleEncapsulatedInterface(byte[] requestBytes) - { - var responseBytes = new List(); - responseBytes.AddRange(requestBytes.Take(2)); - - if (requestBytes[2] != 0x0E) - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalFunction); - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - if (0x06 < requestBytes[4] && requestBytes[4] < 0x80) - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress); - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - var category = (ModbusDeviceIdentificationCategory)requestBytes[3]; - if (!Enum.IsDefined(typeof(ModbusDeviceIdentificationCategory), category)) - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); - - AddCrc(responseBytes); - return [.. responseBytes]; - } - - try - { - var bodyBytes = new List(); - // MEI, Category - bodyBytes.AddRange(requestBytes.Skip(2).Take(2)); - // Conformity - bodyBytes.Add((byte)(category + 0x80)); - // More, NextId, NumberOfObjects - bodyBytes.AddRange(new byte[3]); - - int maxObjectId; - switch (category) - { - case ModbusDeviceIdentificationCategory.Basic: - maxObjectId = 0x02; - break; - - case ModbusDeviceIdentificationCategory.Regular: - maxObjectId = 0x06; - break; - - case ModbusDeviceIdentificationCategory.Extended: - maxObjectId = 0xFF; - break; - - default: // Individual - { - if (requestBytes[4] < 0x03) - bodyBytes[2] = 0x81; - else if (requestBytes[4] < 0x80) - bodyBytes[2] = 0x82; - else - bodyBytes[2] = 0x83; - - maxObjectId = requestBytes[4]; - } - - break; - } - - byte numberOfObjects = 0; - for (int i = requestBytes[4]; i <= maxObjectId; i++) - { - // Reserved - if (0x07 <= i && i <= 0x7F) - continue; - - byte[] objBytes = GetDeviceObject((byte)i); - - // We need to split the response if it would exceed the max ADU size - if (responseBytes.Count + bodyBytes.Count + objBytes.Length > RtuProtocol.MAX_ADU_LENGTH) - { - bodyBytes[3] = 0xFF; - bodyBytes[4] = (byte)i; - - bodyBytes[5] = numberOfObjects; - responseBytes.AddRange(bodyBytes); - return [.. responseBytes]; - } - - bodyBytes.AddRange(objBytes); - numberOfObjects++; - } - - bodyBytes[5] = numberOfObjects; - responseBytes.AddRange(bodyBytes); - - AddCrc(responseBytes); - return [.. responseBytes]; - } - catch - { - responseBytes[1] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); - - AddCrc(responseBytes); - return [.. responseBytes]; - } - } - - private byte[] GetDeviceObject(byte objectId) - { - var result = new List { objectId }; - switch ((ModbusDeviceIdentificationObject)objectId) - { - case ModbusDeviceIdentificationObject.VendorName: - { - byte[] bytes = Encoding.UTF8.GetBytes("AMWD"); - result.Add((byte)bytes.Length); - result.AddRange(bytes); - } - break; - - case ModbusDeviceIdentificationObject.ProductCode: - { - byte[] bytes = Encoding.UTF8.GetBytes("AMWD-MBS-RTU"); - result.Add((byte)bytes.Length); - result.AddRange(bytes); - } - break; - - case ModbusDeviceIdentificationObject.MajorMinorRevision: - { - string version = GetType().Assembly - .GetCustomAttribute() - .InformationalVersion; - - byte[] bytes = Encoding.UTF8.GetBytes(version); - result.Add((byte)bytes.Length); - result.AddRange(bytes); - } - break; - - case ModbusDeviceIdentificationObject.VendorUrl: - { - byte[] bytes = Encoding.UTF8.GetBytes("https://github.com/AM-WD/AMWD.Protocols.Modbus"); - result.Add((byte)bytes.Length); - result.AddRange(bytes); - } - break; - - case ModbusDeviceIdentificationObject.ProductName: - { - byte[] bytes = Encoding.UTF8.GetBytes("AM.WD Modbus Library"); - result.Add((byte)bytes.Length); - result.AddRange(bytes); - } - break; - - case ModbusDeviceIdentificationObject.ModelName: - { - byte[] bytes = Encoding.UTF8.GetBytes("Serial Server"); - result.Add((byte)bytes.Length); - result.AddRange(bytes); - } - break; - - case ModbusDeviceIdentificationObject.UserApplicationName: - { - byte[] bytes = Encoding.UTF8.GetBytes("Modbus RTU Server"); - result.Add((byte)bytes.Length); - result.AddRange(bytes); - } - break; - - default: - result.Add(0x00); - break; - } - - return [.. result]; - } - - private static void SetCrc(byte[] bytes) - { - byte[] crc = RtuProtocol.CRC16(bytes, 0, bytes.Length - 2); - bytes[bytes.Length - 2] = crc[0]; - bytes[bytes.Length - 1] = crc[1]; - } - - private static void AddCrc(List bytes) - { - byte[] crc = RtuProtocol.CRC16(bytes); - bytes.Add(crc[0]); - bytes.Add(crc[1]); - } - - #endregion Request Handling - #region Device Handling - /// - /// Adds a new device to the server. - /// - /// The unit ID of the device. - /// if the device was added, otherwise. + /// public bool AddDevice(byte unitId) - { - Assertions(); + => TypedClient.AddDevice(unitId); - using (_deviceListLock.GetWriteLock()) - { - if (_devices.ContainsKey(unitId)) - return false; - - _devices.Add(unitId, new ModbusDevice(unitId)); - return true; - } - } - - /// - /// Removes a device from the server. - /// - /// The unit ID of the device. - /// if the device was removed, otherwise. + /// public bool RemoveDevice(byte unitId) - { - Assertions(); - - using (_deviceListLock.GetWriteLock()) - { - if (_devices.TryGetValue(unitId, out var device)) - device.Dispose(); - - return _devices.Remove(unitId); - } - } - - /// - /// Gets a from the specified . - /// - /// The unit ID of the device. - /// The address of the coil. - public Coil GetCoil(byte unitId, ushort address) - { - Assertions(); - - using (_deviceListLock.GetReadLock()) - { - if (!_devices.TryGetValue(unitId, out var device)) - return null; - - return device.GetCoil(address); - } - } - - /// - /// Sets a to the specified . - /// - /// The unit ID of the device. - /// The to set. - public void SetCoil(byte unitId, Coil coil) - { - Assertions(); - - using (_deviceListLock.GetReadLock()) - { - if (!_devices.TryGetValue(unitId, out var device)) - return; - - device.SetCoil(coil); - } - } - - /// - /// Gets a from the specified . - /// - /// The unit ID of the device. - /// The address of the . - public DiscreteInput GetDiscreteInput(byte unitId, ushort address) - { - Assertions(); - - using (_deviceListLock.GetReadLock()) - { - if (!_devices.TryGetValue(unitId, out var device)) - return null; - - return device.GetDiscreteInput(address); - } - } - - /// - /// Sets a to the specified . - /// - /// The unit ID of the device. - /// The to set. - public void SetDiscreteInput(byte unitId, DiscreteInput discreteInput) - { - Assertions(); - - using (_deviceListLock.GetReadLock()) - { - if (!_devices.TryGetValue(unitId, out var device)) - return; - - device.SetDiscreteInput(discreteInput); - } - } - - /// - /// Gets a from the specified . - /// - /// The unit ID of the device. - /// The address of the . - public HoldingRegister GetHoldingRegister(byte unitId, ushort address) - { - Assertions(); - - using (_deviceListLock.GetReadLock()) - { - if (!_devices.TryGetValue(unitId, out var device)) - return null; - - return device.GetHoldingRegister(address); - } - } - - /// - /// Sets a to the specified . - /// - /// The unit ID of the device. - /// The to set. - public void SetHoldingRegister(byte unitId, HoldingRegister holdingRegister) - { - Assertions(); - - using (_deviceListLock.GetReadLock()) - { - if (!_devices.TryGetValue(unitId, out var device)) - return; - - device.SetHoldingRegister(holdingRegister); - } - } - - /// - /// Gets a from the specified . - /// - /// The unit ID of the device. - /// The address of the . - public InputRegister GetInputRegister(byte unitId, ushort address) - { - Assertions(); - - using (_deviceListLock.GetReadLock()) - { - if (!_devices.TryGetValue(unitId, out var device)) - return null; - - return device.GetInputRegister(address); - } - } - - /// - /// Sets a to the specified . - /// - /// The unit ID of the device. - /// The to set. - public void SetInputRegister(byte unitId, InputRegister inputRegister) - { - Assertions(); - - using (_deviceListLock.GetReadLock()) - { - if (!_devices.TryGetValue(unitId, out var device)) - return; - - device.SetInputRegister(inputRegister); - } - } + => TypedClient.RemoveDevice(unitId); #endregion Device Handling + + #region Entity Handling + + /// + public Coil GetCoil(byte unitId, ushort address) + => TypedClient.GetCoil(unitId, address); + + /// + public void SetCoil(byte unitId, Coil coil) + => TypedClient.SetCoil(unitId, coil); + + /// + public DiscreteInput GetDiscreteInput(byte unitId, ushort address) + => TypedClient.GetDiscreteInput(unitId, address); + + /// + public void SetDiscreteInput(byte unitId, DiscreteInput discreteInput) + => TypedClient.SetDiscreteInput(unitId, discreteInput); + + /// + public HoldingRegister GetHoldingRegister(byte unitId, ushort address) + => TypedClient.GetHoldingRegister(unitId, address); + + /// + public void SetHoldingRegister(byte unitId, HoldingRegister holdingRegister) + => TypedClient.SetHoldingRegister(unitId, holdingRegister); + + /// + public InputRegister GetInputRegister(byte unitId, ushort address) + => TypedClient.GetInputRegister(unitId, address); + + /// + public void SetInputRegister(byte unitId, InputRegister inputRegister) + => TypedClient.SetInputRegister(unitId, inputRegister); + + #endregion Entity Handling } } diff --git a/AMWD.Protocols.Modbus.Serial/Utils/SerialPortWrapper.cs b/AMWD.Protocols.Modbus.Serial/Utils/SerialPortWrapper.cs index 305ae20..c622c68 100644 --- a/AMWD.Protocols.Modbus.Serial/Utils/SerialPortWrapper.cs +++ b/AMWD.Protocols.Modbus.Serial/Utils/SerialPortWrapper.cs @@ -24,9 +24,9 @@ namespace AMWD.Protocols.Modbus.Serial.Utils public SerialPortWrapper() { - _serialPort.DataReceived += OnDataReceived; - _serialPort.PinChanged += OnPinChanged; - _serialPort.ErrorReceived += OnErrorReceived; + _serialPort.DataReceived += (sender, e) => DataReceived?.Invoke(this, e); + _serialPort.PinChanged += (sender, e) => PinChanged?.Invoke(this, e); + _serialPort.ErrorReceived += (sender, e) => ErrorReceived?.Invoke(this, e); } #endregion Constructor @@ -42,15 +42,6 @@ namespace AMWD.Protocols.Modbus.Serial.Utils /// public virtual event SerialErrorReceivedEventHandler ErrorReceived; - private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) - => DataReceived?.Invoke(sender, e); - - private void OnPinChanged(object sender, SerialPinChangedEventArgs e) - => PinChanged?.Invoke(sender, e); - - private void OnErrorReceived(object sender, SerialErrorReceivedEventArgs e) - => ErrorReceived?.Invoke(sender, e); - #endregion Events #region Properties diff --git a/AMWD.Protocols.Modbus.Tests/AMWD.Protocols.Modbus.Tests.csproj b/AMWD.Protocols.Modbus.Tests/AMWD.Protocols.Modbus.Tests.csproj index fdc9b15..5d85284 100644 --- a/AMWD.Protocols.Modbus.Tests/AMWD.Protocols.Modbus.Tests.csproj +++ b/AMWD.Protocols.Modbus.Tests/AMWD.Protocols.Modbus.Tests.csproj @@ -6,24 +6,21 @@ false true true + Cobertura false false - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/AMWD.Protocols.Modbus.Tests/Helper.cs b/AMWD.Protocols.Modbus.Tests/Helper.cs new file mode 100644 index 0000000..3b101c4 --- /dev/null +++ b/AMWD.Protocols.Modbus.Tests/Helper.cs @@ -0,0 +1,23 @@ +using System.Reflection; + +namespace AMWD.Protocols.Modbus.Tests +{ + internal static class Helper + { + public static T CreateInstance(params object[] args) + { + var type = typeof(T); + + object instance = type.Assembly.CreateInstance( + typeName: type.FullName, + ignoreCase: false, + bindingAttr: BindingFlags.Instance | BindingFlags.NonPublic, + binder: null, + args: args, + culture: null, + activationAttributes: null); + + return (T)instance; + } + } +} diff --git a/AMWD.Protocols.Modbus.Tests/Serial/ModbusRtuProxyTest.cs b/AMWD.Protocols.Modbus.Tests/Serial/ModbusRtuProxyTest.cs new file mode 100644 index 0000000..cc7a3b3 --- /dev/null +++ b/AMWD.Protocols.Modbus.Tests/Serial/ModbusRtuProxyTest.cs @@ -0,0 +1,2006 @@ +using System.Collections.Generic; +using System.IO.Ports; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using AMWD.Protocols.Modbus.Common.Protocols; +using AMWD.Protocols.Modbus.Serial; +using AMWD.Protocols.Modbus.Serial.Utils; +using Moq; + +namespace AMWD.Protocols.Modbus.Tests.Serial +{ + [TestClass] + public class ModbusRtuProxyTest + { + private Mock _clientMock; + private Mock _serialPortMock; + + private SerialDataReceivedEventArgs _dataReceivedEventArgs; + private Queue _requestBytesQueue; + private List _responseBytesCallbacks; + + #region Read functions + + private List<(byte UnitId, ushort StartAddress, ushort Count)> _clientReadCallbacks; + private List<(byte UnitId, ModbusDeviceIdentificationCategory Category, ModbusDeviceIdentificationObject ObjectId)> _clientReadDeviceCallbacks; + private List _clientReadCoilsResponse; + private List _clientReadDiscreteInputsResponse; + private List _clientReadHoldingRegistersResponse; + private List _clientReadInputRegistersResponse; + private DeviceIdentification _clientDeviceIdentificationResponse; + + #endregion Read functions + + #region Write functions + + private List<(byte UnitId, Coil Coil)> _writeSingleCoilCallbacks; + private List<(byte UnitId, List Coils)> _writeMultipleCoilsCallbacks; + private List<(byte UnitId, HoldingRegister HoldingRegister)> _writeSingleRegisterCallbacks; + private List<(byte UnitId, List HoldingRegisters)> _writeMultipleRegistersCallbacks; + + private bool _clientWriteResponse; + + #endregion Write functions + + [TestInitialize] + public void Initialize() + { + _dataReceivedEventArgs = Helper.CreateInstance(SerialData.Chars); + _requestBytesQueue = new Queue(); + _responseBytesCallbacks = []; + + #region Read functions + + _clientReadCallbacks = []; + _clientReadDeviceCallbacks = []; + _clientReadCoilsResponse = []; + _clientReadDiscreteInputsResponse = []; + _clientReadHoldingRegistersResponse = []; + _clientReadInputRegistersResponse = []; + _clientDeviceIdentificationResponse = new DeviceIdentification + { + VendorName = nameof(DeviceIdentification.VendorName), + ProductCode = nameof(DeviceIdentification.ProductCode), + MajorMinorRevision = nameof(DeviceIdentification.MajorMinorRevision), + }; + _clientDeviceIdentificationResponse.ExtendedObjects.Add(131, [11, 22, 33]); + + #endregion Read functions + + #region Write functions + + _writeSingleCoilCallbacks = []; + _writeMultipleCoilsCallbacks = []; + _writeSingleRegisterCallbacks = []; + _writeMultipleRegistersCallbacks = []; + + _clientWriteResponse = true; + + #endregion Write functions + } + + #region General + + [TestMethod] + public void ShouldCreateInstance() + { + // Arrange + + // Act + using (var proxy = GetProxy()) + { + // Assert + Assert.IsNotNull(proxy); + } + + // Assert + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Dispose(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public void ShouldDispose() + { + // Arrange + + // Act + using (var proxy = GetProxy()) + { + proxy.Dispose(); + } + + // Assert + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Dispose(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldStartAndStop() + { + // Arrange + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await proxy.StopAsync(); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Exactly(2)); + + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Close(), Times.Exactly(2)); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Exactly(2)); + + VerifyNoOtherCalls(); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void ShouldThrowArgumentNullExceptionOnCreateInstanceForClient() + { + // Arrange + + // Act + new ModbusRtuProxy(null, "some-port"); + + // Assert - ArgumentNullException + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + [ExpectedException(typeof(ArgumentNullException))] + public void ShouldThrowArgumentNullExceptionOnCreateInstanceForPortName(string portName) + { + // Arrange + var connection = new Mock(); + var clientMock = new Mock(connection.Object); + + // Act + new ModbusRtuProxy(clientMock.Object, portName); + + // Assert - ArgumentNullException + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + [ExpectedException(typeof(ArgumentNullException))] + public async Task ShouldThrowArgumentNullExceptionOnMissingPortName(string portName) + { + // Arrange + using var proxy = GetProxy(); + _serialPortMock.Setup(m => m.PortName).Returns(portName); + + // Act + await proxy.StartAsync(); + + // Assert - ArgumentNullException + } + + [TestMethod] + public void ShouldGetAllProperties() + { + // Arrange + using var proxy = GetProxy(); + + // Act + Assert.AreEqual("some-port", proxy.PortName); + Assert.AreEqual(BaudRate.Baud19200, proxy.BaudRate); + Assert.AreEqual(8, proxy.DataBits); + Assert.AreEqual(Handshake.None, proxy.Handshake); + Assert.AreEqual(Parity.Even, proxy.Parity); + Assert.IsFalse(proxy.RtsEnable); + Assert.AreEqual(StopBits.One, proxy.StopBits); + Assert.IsTrue(proxy.IsOpen); + Assert.AreEqual(1000, proxy.ReadTimeout.TotalMilliseconds); + Assert.AreEqual(1000, proxy.WriteTimeout.TotalMilliseconds); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BaudRate, Times.Once); + _serialPortMock.VerifyGet(m => m.DataBits, Times.Once); + _serialPortMock.VerifyGet(m => m.Handshake, Times.Once); + _serialPortMock.VerifyGet(m => m.Parity, Times.Once); + _serialPortMock.VerifyGet(m => m.RtsEnable, Times.Once); + _serialPortMock.VerifyGet(m => m.StopBits, Times.Once); + _serialPortMock.VerifyGet(m => m.IsOpen, Times.Once); + _serialPortMock.VerifyGet(m => m.ReadTimeout, Times.Once); + _serialPortMock.VerifyGet(m => m.WriteTimeout, Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public void ShouldSetAllProperties() + { + // Arrange + using var proxy = GetProxy(); + + // Act + proxy.PortName = "other-port"; + proxy.BaudRate = BaudRate.Baud115200; + proxy.DataBits = 7; + proxy.Handshake = Handshake.RequestToSend; + proxy.Parity = Parity.Odd; + proxy.RtsEnable = true; + proxy.StopBits = StopBits.OnePointFive; + proxy.ReadTimeout = TimeSpan.FromSeconds(5); + proxy.WriteTimeout = TimeSpan.FromSeconds(10); + + // Assert + _serialPortMock.VerifySet(m => m.PortName = "other-port", Times.Once); + _serialPortMock.VerifySet(m => m.BaudRate = 115200, Times.Once); + _serialPortMock.VerifySet(m => m.DataBits = 7, Times.Once); + _serialPortMock.VerifySet(m => m.Handshake = Handshake.RequestToSend, Times.Once); + _serialPortMock.VerifySet(m => m.Parity = Parity.Odd, Times.Once); + _serialPortMock.VerifySet(m => m.RtsEnable = true, Times.Once); + _serialPortMock.VerifySet(m => m.StopBits = StopBits.OnePointFive, Times.Once); + _serialPortMock.VerifySet(m => m.ReadTimeout = 5000, Times.Once); + _serialPortMock.VerifySet(m => m.WriteTimeout = 10000, Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldIgnoreExceptionInDataReceived() + { + // Arrange + // Not adding request data to the queue will cause an exception while reading + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldSkipOnCrcError() + { + // Arrange + byte[] request = [2, 1, 0, 5, 0, 4, 0, 0]; + _requestBytesQueue.Enqueue(request); + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldReturnIllegalFunction() + { + // Arrange + byte[] request = [1, 14, 0, 5, 0, 4]; + _requestBytesQueue.Enqueue(request); + _requestBytesQueue.Enqueue(RtuProtocol.CRC16(request)); + byte[] expectedResponse = [1, 142, 1]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Exactly(2)); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Exactly(2)); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 5), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + #endregion General + + #region Read functions + + #region Read Coils (Fn 1) + + [TestMethod] + public async Task ShouldReadCoils() + { + // Arrange + byte[] request = [2, 1, 0, 5, 0, 4]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + _clientReadCoilsResponse = [ + new Coil { Address = 5, HighByte = 0xFF }, + new Coil { Address = 6, HighByte = 0x00 }, + new Coil { Address = 7, HighByte = 0x00 }, + new Coil { Address = 8, HighByte = 0xFF }, + ]; + byte[] expectedResponse = [2, 1, 1, 9]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 6), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.ReadCoilsAsync(2, 5, 4, It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + var (unitId, startAddress, count) = _clientReadCallbacks.First(); + Assert.AreEqual(2, unitId); + Assert.AreEqual(5, startAddress); + Assert.AreEqual(4, count); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldIgnoreTooShortRequestOnReadCoils() + { + // Arrange + byte[] request = [2, 1, 0, 5]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnReadCoils() + { + // Arrange + byte[] request = [2, 1, 0, 5, 0, 4]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [2, 129, 4]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + _clientMock + .Setup(m => m.ReadCoilsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((unitId, address, count, _) => _clientReadCallbacks.Add((unitId, address, count))) + .ThrowsAsync(new Exception("Error ;-)")); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 5), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.ReadCoilsAsync(2, 5, 4, It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + #endregion Read Coils (Fn 1) + + #region Read Discrete Inputs (Fn 2) + + [TestMethod] + public async Task ShouldReadDiscreteInputs() + { + // Arrange + byte[] request = [22, 2, 0, 5, 0, 4]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + _clientReadDiscreteInputsResponse = [ + new DiscreteInput { Address = 5, HighByte = 0x00 }, + new DiscreteInput { Address = 6, HighByte = 0xFF }, + new DiscreteInput { Address = 7, HighByte = 0x00 }, + new DiscreteInput { Address = 8, HighByte = 0xFF }, + ]; + byte[] expectedResponse = [22, 2, 1, 10]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 6), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.ReadDiscreteInputsAsync(22, 5, 4, It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + var (unitId, startAddress, count) = _clientReadCallbacks.First(); + Assert.AreEqual(22, unitId); + Assert.AreEqual(5, startAddress); + Assert.AreEqual(4, count); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldIgnoreTooShortRequestOnReadDiscreteInputs() + { + // Arrange + byte[] request = [2, 2, 0, 5]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnDiscreteInputs() + { + // Arrange + byte[] request = [2, 2, 0, 5, 0, 4]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [2, 130, 4]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + _clientMock + .Setup(m => m.ReadDiscreteInputsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((unitId, address, count, _) => _clientReadCallbacks.Add((unitId, address, count))) + .ThrowsAsync(new Exception("Error ;-)")); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 5), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.ReadDiscreteInputsAsync(2, 5, 4, It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + #endregion Read Discrete Inputs (Fn 2) + + #region Read Holding Registers (Fn 3) + + [TestMethod] + public async Task ShouldReadHoldingRegisters() + { + // Arrange + byte[] request = [42, 3, 0, 15, 0, 2]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + _clientReadHoldingRegistersResponse = [ + new HoldingRegister { Address = 15, LowByte = 12, HighByte = 34 }, + new HoldingRegister { Address = 16, LowByte = 56, HighByte = 78 }, + ]; + byte[] expectedResponse = [42, 3, 4, 34, 12, 78, 56]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 9), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.ReadHoldingRegistersAsync(42, 15, 2, It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + var (unitId, startAddress, count) = _clientReadCallbacks.First(); + Assert.AreEqual(42, unitId); + Assert.AreEqual(15, startAddress); + Assert.AreEqual(2, count); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldIgnoreTooShortRequestOnReadHoldingRegisters() + { + // Arrange + byte[] request = [2, 3, 0, 5]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnReadHoldingRegisters() + { + // Arrange + byte[] request = [2, 3, 0, 5, 0, 4]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [2, 131, 4]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + _clientMock + .Setup(m => m.ReadHoldingRegistersAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((unitId, address, count, _) => _clientReadCallbacks.Add((unitId, address, count))) + .ThrowsAsync(new Exception("Error ;-)")); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 5), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.ReadHoldingRegistersAsync(2, 5, 4, It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + #endregion Read Holding Registers (Fn 3) + + #region Read Input Registers (Fn 4) + + [TestMethod] + public async Task ShouldReadInputRegisters() + { + // Arrange + byte[] request = [42, 4, 0, 15, 0, 2]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + _clientReadInputRegistersResponse = [ + new InputRegister { Address = 15, LowByte = 34, HighByte = 12 }, + new InputRegister { Address = 16, LowByte = 78, HighByte = 56 }, + ]; + byte[] expectedResponse = [42, 4, 4, 12, 34, 56, 78]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 9), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.ReadInputRegistersAsync(42, 15, 2, It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + var (unitId, startAddress, count) = _clientReadCallbacks.First(); + Assert.AreEqual(42, unitId); + Assert.AreEqual(15, startAddress); + Assert.AreEqual(2, count); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldIgnoreTooShortRequestOnReadInputRegisters() + { + // Arrange + byte[] request = [2, 4, 0, 5]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnReadInputRegisters() + { + // Arrange + byte[] request = [2, 4, 0, 5, 0, 4]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [2, 132, 4]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + _clientMock + .Setup(m => m.ReadInputRegistersAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((unitId, address, count, _) => _clientReadCallbacks.Add((unitId, address, count))) + .ThrowsAsync(new Exception("Error ;-)")); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 5), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.ReadInputRegistersAsync(2, 5, 4, It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + #endregion Read Input Registers (Fn 4) + + #region Read Encapsulated Interface (Fn 43) + + [TestMethod] + public async Task ShouldReadDeviceIdentification() + { + // Arrange + byte[] request = [1, 43, 14, 1, 0]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [ + 1, 43, 14, 1, + 1, 0, 0, 3, + 0, 10, 86, 101, 110, 100, 111, 114, 78, 97, 109, 101, + 1, 11, 80, 114, 111, 100, 117, 99, 116, 67, 111, 100, 101, + 2, 18, 77, 97, 106, 111, 114, 77, 105, 110, 111, 114, 82, 101, 118, 105, 115, 105, 111, 110]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 55), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.ReadDeviceIdentificationAsync(1, ModbusDeviceIdentificationCategory.Basic, ModbusDeviceIdentificationObject.VendorName, It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + var (unitId, category, objectId) = _clientReadDeviceCallbacks.First(); + Assert.AreEqual(1, unitId); + Assert.AreEqual(ModbusDeviceIdentificationCategory.Basic, category); + Assert.AreEqual(ModbusDeviceIdentificationObject.VendorName, objectId); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + SnapshotAssert.AreEqual(_clientDeviceIdentificationResponse.ToString()); + } + + [TestMethod] + public async Task ShouldIgnoreTooShortRequestOnReadDeviceIdentification() + { + // Arrange + byte[] request = [1, 43, 14, 1]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldReturnIllegalFunctionForWrongTypeOnReadDeviceIdentification() + { + // Arrange + byte[] request = [1, 43, 13, 1, 0]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [1, 171, 1]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 5), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReturnIllegalDataAddressForWrongTypeOnReadDeviceIdentification() + { + // Arrange + byte[] request = [1, 43, 14, 4, 10]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [1, 171, 2]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 5), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReturnIllegalDataValueForWrongTypeOnReadDeviceIdentification() + { + // Arrange + byte[] request = [1, 43, 14, 0, 0]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [1, 171, 3]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 5), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReadDeviceIdentificationWithIndividualAccessAllowed() + { + // Arrange + byte[] request = [1, 43, 14, 1, 0]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + _clientDeviceIdentificationResponse.IsIndividualAccessAllowed = true; + byte[] expectedResponse = [ + 1, 43, 14, 1, + 129, 0, 0, 3, + 0, 10, 86, 101, 110, 100, 111, 114, 78, 97, 109, 101, + 1, 11, 80, 114, 111, 100, 117, 99, 116, 67, 111, 100, 101, + 2, 18, 77, 97, 106, 111, 114, 77, 105, 110, 111, 114, 82, 101, 118, 105, 115, 105, 111, 110]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 55), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.ReadDeviceIdentificationAsync(1, ModbusDeviceIdentificationCategory.Basic, ModbusDeviceIdentificationObject.VendorName, It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + var (unitId, category, objectId) = _clientReadDeviceCallbacks.First(); + Assert.AreEqual(1, unitId); + Assert.AreEqual(ModbusDeviceIdentificationCategory.Basic, category); + Assert.AreEqual(ModbusDeviceIdentificationObject.VendorName, objectId); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReadDeviceIdentificationRegular() + { + // Arrange + byte[] request = [1, 43, 14, 2, 0]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [ + 1, 43, 14, 2, + 2, 0, 0, 7, + 0, 10, 86, 101, 110, 100, 111, 114, 78, 97, 109, 101, + 1, 11, 80, 114, 111, 100, 117, 99, 116, 67, 111, 100, 101, + 2, 18, 77, 97, 106, 111, 114, 77, 105, 110, 111, 114, 82, 101, 118, 105, 115, 105, 111, 110, + 3, 0, 4, 0, 5, 0, 6, 0,]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 63), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.ReadDeviceIdentificationAsync(1, ModbusDeviceIdentificationCategory.Regular, ModbusDeviceIdentificationObject.VendorName, It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + var (unitId, category, objectId) = _clientReadDeviceCallbacks.First(); + Assert.AreEqual(1, unitId); + Assert.AreEqual(ModbusDeviceIdentificationCategory.Regular, category); + Assert.AreEqual(ModbusDeviceIdentificationObject.VendorName, objectId); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReadDeviceIdentificationExtended() + { + // Arrange + byte[] request = [1, 43, 14, 3, 0]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [ + 1, 43, 14, 3, + 3, 255, 224, 103, + 0, 10, 86, 101, 110, 100, 111, 114, 78, 97, 109, 101, + 1, 11, 80, 114, 111, 100, 117, 99, 116, 67, 111, 100, 101, + 2, 18, 77, 97, 106, 111, 114, 77, 105, 110, 111, 114, 82, 101, 118, 105, 115, 105, 111, 110, + 3, 0, 4, 0, 5, 0, 6, 0, + 128, 0, 129, 0, 130, 0, + 131, 3, 11, 22, 33, + 132, 0, 133, 0, 134, 0, 135, 0, 136, 0, 137, 0, 138, 0, 139, 0, 140, 0, 141, 0, 142, 0, 143, 0, 144, 0, 145, 0, 146, 0, 147, 0, 148, 0, 149, 0, 150, 0, 151, 0, 152, 0, 153, 0, 154, 0, 155, 0, 156, 0, 157, 0, 158, 0, 159, 0, 160, 0, 161, 0, 162, 0, 163, 0, 164, 0, 165, 0, 166, 0, 167, 0, 168, 0, 169, 0, 170, 0, 171, 0, 172, 0, 173, 0, 174, 0, 175, 0, 176, 0, 177, 0, 178, 0, 179, 0, 180, 0, 181, 0, 182, 0, 183, 0, 184, 0, 185, 0, 186, 0, 187, 0, 188, 0, 189, 0, 190, 0, 191, 0, 192, 0, 193, 0, 194, 0, 195, 0, 196, 0, 197, 0, 198, 0, 199, 0, 200, 0, 201, 0, 202, 0, 203, 0, 204, 0, 205, 0, 206, 0, 207, 0, 208, 0, 209, 0, 210, 0, 211, 0, 212, 0, 213, 0, 214, 0, 215, 0, 216, 0, 217, 0, 218, 0, 219, 0, 220, 0, 221, 0, 222, 0, 223, 0]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 258), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.ReadDeviceIdentificationAsync(1, ModbusDeviceIdentificationCategory.Extended, ModbusDeviceIdentificationObject.VendorName, It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + var (unitId, category, objectId) = _clientReadDeviceCallbacks.First(); + Assert.AreEqual(1, unitId); + Assert.AreEqual(ModbusDeviceIdentificationCategory.Extended, category); + Assert.AreEqual(ModbusDeviceIdentificationObject.VendorName, objectId); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReadDeviceIdentificationIndividual() + { + // Arrange + byte[] request = [1, 43, 14, 4, 0]; + _clientDeviceIdentificationResponse.IsIndividualAccessAllowed = true; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [ + 1, 43, 14, 4, + 132, 0, 0, 1, + 0, 10, 86, 101, 110, 100, 111, 114, 78, 97, 109, 101]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 22), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.ReadDeviceIdentificationAsync(1, ModbusDeviceIdentificationCategory.Individual, ModbusDeviceIdentificationObject.VendorName, It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + var (unitId, category, objectId) = _clientReadDeviceCallbacks.First(); + Assert.AreEqual(1, unitId); + Assert.AreEqual(ModbusDeviceIdentificationCategory.Individual, category); + Assert.AreEqual(ModbusDeviceIdentificationObject.VendorName, objectId); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureForWrongTypeOnReadDeviceIdentification() + { + // Arrange + byte[] request = [1, 43, 14, 1, 0]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [1, 171, 4]; + + using var proxy = GetProxy(); + _clientMock.Setup(m => m.ReadDeviceIdentificationAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((unitId, category, objectId, _) => _clientReadDeviceCallbacks.Add((unitId, category, objectId))) + .ThrowsAsync(new ModbusException()); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 5), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.ReadDeviceIdentificationAsync(1, ModbusDeviceIdentificationCategory.Basic, ModbusDeviceIdentificationObject.VendorName, It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + var (unitId, category, objectId) = _clientReadDeviceCallbacks.First(); + Assert.AreEqual(1, unitId); + Assert.AreEqual(ModbusDeviceIdentificationCategory.Basic, category); + Assert.AreEqual(ModbusDeviceIdentificationObject.VendorName, objectId); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + #endregion Read Encapsulated Interface (Fn 43) + + #endregion Read functions + + #region Write functions + + #region Write Single Coil (Fn 5) + + [TestMethod] + public async Task ShouldWriteSingleCoil() + { + // Arrange + byte[] request = [3, 5, 0, 7, 255, 0]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [3, 5, 0, 7, 255, 0]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 8), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.WriteSingleCoilAsync(3, It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + var (unitId, coil) = _writeSingleCoilCallbacks.First(); + Assert.AreEqual(3, unitId); + Assert.AreEqual(7, coil.Address); + Assert.IsTrue(coil.Value); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldIgnoreTooShortRequestOnWriteSingleCoil() + { + // Arrange + byte[] request = [3, 5, 0, 7, 255]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldReturnIllegalDataValueOnWriteSingleCoil() + { + // Arrange + byte[] request = [3, 5, 0, 7, 250, 0]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [3, 133, 3]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 5), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnWriteSingleCoilForNotSuccessful() + { + // Arrange + _clientWriteResponse = false; + byte[] request = [3, 5, 0, 7, 255, 0]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [3, 133, 4]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 5), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.WriteSingleCoilAsync(3, It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + var (unitId, coil) = _writeSingleCoilCallbacks.First(); + Assert.AreEqual(3, unitId); + Assert.AreEqual(7, coil.Address); + Assert.IsTrue(coil.Value); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnWriteSingleCoilForException() + { + // Arrange + byte[] request = [3, 5, 0, 7, 255, 0]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [3, 133, 4]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + _clientMock + .Setup(m => m.WriteSingleCoilAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((unitId, coil, _) => _writeSingleCoilCallbacks.Add((unitId, coil))) + .ThrowsAsync(new ModbusException()); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 5), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.WriteSingleCoilAsync(3, It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + var (unitId, coil) = _writeSingleCoilCallbacks.First(); + Assert.AreEqual(3, unitId); + Assert.AreEqual(7, coil.Address); + Assert.IsTrue(coil.Value); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + #endregion Write Single Coil (Fn 5) + + #region Write Single Register (Fn 6) + + [TestMethod] + public async Task ShouldWriteSingleRegister() + { + // Arrange + byte[] request = [4, 6, 0, 1, 0, 3]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [4, 6, 0, 1, 0, 3]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 8), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.WriteSingleHoldingRegisterAsync(4, It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + var (unitId, register) = _writeSingleRegisterCallbacks.First(); + Assert.AreEqual(4, unitId); + Assert.AreEqual(1, register.Address); + Assert.AreEqual(3, register.Value); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldIgnoreTooShortRequestOnWriteSingleRegister() + { + // Arrange + byte[] request = [4, 6, 0, 1, 0]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnWriteSingleRegisterForNotSuccessful() + { + // Arrange + _clientWriteResponse = false; + byte[] request = [4, 6, 0, 1, 0, 3]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [4, 134, 4]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 5), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.WriteSingleHoldingRegisterAsync(4, It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + var (unitId, register) = _writeSingleRegisterCallbacks.First(); + Assert.AreEqual(4, unitId); + Assert.AreEqual(1, register.Address); + Assert.AreEqual(3, register.Value); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnWriteSingleRegisterForException() + { + // Arrange + byte[] request = [4, 6, 0, 1, 0, 3]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [4, 134, 4]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + _clientMock + .Setup(m => m.WriteSingleHoldingRegisterAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((unitId, register, _) => _writeSingleRegisterCallbacks.Add((unitId, register))) + .ThrowsAsync(new ModbusException()); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 5), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.WriteSingleHoldingRegisterAsync(4, It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + var (unitId, register) = _writeSingleRegisterCallbacks.First(); + Assert.AreEqual(4, unitId); + Assert.AreEqual(1, register.Address); + Assert.AreEqual(3, register.Value); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + #endregion Write Single Register (Fn 6) + + #region Write Multiple Coils (Fn 15) + + [TestMethod] + public async Task ShouldWriteMultipleCoils() + { + // Arrange + byte[] request = [1, 15, 0, 13, 0, 10, 2, 205, 1]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [1, 15, 0, 13, 0, 10]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 8), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.WriteMultipleCoilsAsync(1, It.IsAny>(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + + var (unitId, coils) = _writeMultipleCoilsCallbacks.First(); + Assert.AreEqual(1, unitId); + Assert.AreEqual(10, coils.Count); + + for (byte i = 13; i < 23; i++) + Assert.IsNotNull(coils.Where(c => c.Address == i).FirstOrDefault()); + + CollectionAssert.AreEqual(new bool[] { true, false, true, true, false, false, true, true, true, false }, coils.Select(c => c.Value).ToArray()); + } + + [TestMethod] + public async Task ShouldIgnoreTooShortRequestOnWriteMultipleCoils() + { + // Arrange + byte[] request = [1, 15, 0, 13, 0, 10]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldReturnIllegalDataValueOnWriteMultipleCoils() + { + // Arrange + byte[] request = [1, 15, 0, 13, 0, 10, 2, 205]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [1, 143, 3]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 5), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnWriteMultipleCoilsForNotSuccessful() + { + // Arrange + _clientWriteResponse = false; + byte[] request = [1, 15, 0, 13, 0, 10, 2, 205, 1]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [1, 143, 4]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 5), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.WriteMultipleCoilsAsync(1, It.IsAny>(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + + var (unitId, coils) = _writeMultipleCoilsCallbacks.First(); + Assert.AreEqual(1, unitId); + Assert.AreEqual(10, coils.Count); + + for (byte i = 13; i < 23; i++) + Assert.IsNotNull(coils.Where(c => c.Address == i).FirstOrDefault()); + + CollectionAssert.AreEqual(new bool[] { true, false, true, true, false, false, true, true, true, false }, coils.Select(c => c.Value).ToArray()); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnWriteMultipleCoilsForException() + { + // Arrange + byte[] request = [1, 15, 0, 13, 0, 10, 2, 205, 1]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [1, 143, 4]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + _clientMock + .Setup(m => m.WriteMultipleCoilsAsync(It.IsAny(), It.IsAny>(), It.IsAny())) + .Callback, CancellationToken>((unitId, coils, _) => _writeMultipleCoilsCallbacks.Add((unitId, coils.ToList()))) + .ThrowsAsync(new ModbusException()); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 5), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.WriteMultipleCoilsAsync(1, It.IsAny>(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + + var (unitId, coils) = _writeMultipleCoilsCallbacks.First(); + Assert.AreEqual(1, unitId); + Assert.AreEqual(10, coils.Count); + + for (byte i = 13; i < 23; i++) + Assert.IsNotNull(coils.Where(c => c.Address == i).FirstOrDefault()); + + CollectionAssert.AreEqual(new bool[] { true, false, true, true, false, false, true, true, true, false }, coils.Select(c => c.Value).ToArray()); + } + + #endregion Write Multiple Coils (Fn 15) + + #region Write Multiple Coils (Fn 16) + + [TestMethod] + public async Task ShouldWriteMultipleRegisters() + { + // Arrange + byte[] request = [1, 16, 0, 1, 0, 2, 4, 0, 10, 1, 2]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [1, 16, 0, 1, 0, 2]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 8), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.WriteMultipleHoldingRegistersAsync(1, It.IsAny>(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + + var (unitId, registers) = _writeMultipleRegistersCallbacks.First(); + Assert.AreEqual(1, unitId); + Assert.AreEqual(2, registers.Count); + + for (byte i = 1; i < 3; i++) + Assert.IsNotNull(registers.Where(c => c.Address == i).FirstOrDefault()); + + CollectionAssert.AreEqual(new ushort[] { 10, 258 }, registers.Select(c => c.Value).ToArray()); + } + + [TestMethod] + public async Task ShouldIgnoreTooShortRequestOnWriteMultipleRegisters() + { + // Arrange + byte[] request = [1, 16, 0, 1, 0, 2]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldReturnIllegalDataValueOnWriteMultipleRegisters() + { + // Arrange + byte[] request = [1, 16, 0, 1, 0, 2, 4, 0, 10, 1]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [1, 144, 3]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 5), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnWriteMultipleRegistersForNotSuccessful() + { + // Arrange + _clientWriteResponse = false; + byte[] request = [1, 16, 0, 1, 0, 2, 4, 0, 10, 1, 2]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [1, 144, 4]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 5), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.WriteMultipleHoldingRegistersAsync(1, It.IsAny>(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + + var (unitId, registers) = _writeMultipleRegistersCallbacks.First(); + Assert.AreEqual(1, unitId); + Assert.AreEqual(2, registers.Count); + + for (byte i = 1; i < 3; i++) + Assert.IsNotNull(registers.Where(c => c.Address == i).FirstOrDefault()); + + CollectionAssert.AreEqual(new ushort[] { 10, 258 }, registers.Select(c => c.Value).ToArray()); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnWriteMultipleRegistersForException() + { + // Arrange + byte[] request = [1, 16, 0, 1, 0, 2, 4, 0, 10, 1, 2]; + _requestBytesQueue.Enqueue([.. request, .. RtuProtocol.CRC16(request)]); + byte[] expectedResponse = [1, 144, 4]; + + using var proxy = GetProxy(); + await proxy.StartAsync(); + + _clientMock + .Setup(m => m.WriteMultipleHoldingRegistersAsync(It.IsAny(), It.IsAny>(), It.IsAny())) + .Callback, CancellationToken>((unitId, coils, _) => _writeMultipleRegistersCallbacks.Add((unitId, coils.ToList()))) + .ThrowsAsync(new ModbusException()); + + // Act + _serialPortMock.Raise(m => m.DataReceived += null, _dataReceivedEventArgs); + + // Assert + _serialPortMock.VerifyGet(m => m.PortName, Times.Once); + _serialPortMock.VerifyGet(m => m.BytesToRead, Times.Once); + + _serialPortMock.Verify(m => m.Close(), Times.Once); + _serialPortMock.Verify(m => m.Open(), Times.Once); + _serialPortMock.Verify(m => m.Read(It.IsAny(), 0, RtuProtocol.MAX_ADU_LENGTH), Times.Once); + _serialPortMock.Verify(m => m.Write(It.IsAny(), 0, 5), Times.Once); + + _serialPortMock.VerifyAdd(m => m.DataReceived += It.IsAny(), Times.Once); + _serialPortMock.VerifyRemove(m => m.DataReceived -= It.IsAny(), Times.Once); + + _clientMock.Verify(m => m.WriteMultipleHoldingRegistersAsync(1, It.IsAny>(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse.Concat(RtuProtocol.CRC16(expectedResponse)).ToArray(), _responseBytesCallbacks.First()); + + var (unitId, registers) = _writeMultipleRegistersCallbacks.First(); + Assert.AreEqual(1, unitId); + Assert.AreEqual(2, registers.Count); + + for (byte i = 1; i < 3; i++) + Assert.IsNotNull(registers.Where(c => c.Address == i).FirstOrDefault()); + + CollectionAssert.AreEqual(new ushort[] { 10, 258 }, registers.Select(c => c.Value).ToArray()); + } + + #endregion Write Multiple Coils (Fn 16) + + #endregion Write functions + + private void VerifyNoOtherCalls() + { + _clientMock.VerifyNoOtherCalls(); + _serialPortMock.VerifyNoOtherCalls(); + } + + private ModbusRtuProxy GetProxy() + { + var connection = new Mock(); + + _clientMock = new Mock(connection.Object); + _serialPortMock = new Mock(); + + #region General + + _serialPortMock.Setup(m => m.PortName).Returns("some-port"); + _serialPortMock.Setup(m => m.BaudRate).Returns(19200); + _serialPortMock.Setup(m => m.DataBits).Returns(8); + _serialPortMock.Setup(m => m.Handshake).Returns(Handshake.None); + _serialPortMock.Setup(m => m.Parity).Returns(Parity.Even); + _serialPortMock.Setup(m => m.RtsEnable).Returns(false); + _serialPortMock.Setup(m => m.StopBits).Returns(StopBits.One); + _serialPortMock.Setup(m => m.IsOpen).Returns(true); + _serialPortMock.Setup(m => m.ReadTimeout).Returns(1000); + _serialPortMock.Setup(m => m.WriteTimeout).Returns(1000); + + _serialPortMock + .Setup(m => m.BytesToRead) + // This does not reflect the correct value but is sufficient for testing + .Returns(() => _requestBytesQueue.Count); + + _serialPortMock + .Setup(m => m.Read(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((buffer, offset, count) => + { + byte[] bytes = _requestBytesQueue.Dequeue(); + int minLength = Math.Min(bytes.Length, count); + + Array.Copy(bytes, 0, buffer, offset, minLength); + return minLength; + }); + _serialPortMock + .Setup(m => m.Write(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((buffer, _, __) => _responseBytesCallbacks.Add(buffer)); + + #endregion General + + #region Read functions + + _clientMock + .Setup(m => m.ReadCoilsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((unitId, address, count, _) => _clientReadCallbacks.Add((unitId, address, count))) + .ReturnsAsync(() => _clientReadCoilsResponse); + _clientMock + .Setup(m => m.ReadDiscreteInputsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((unitId, address, count, _) => _clientReadCallbacks.Add((unitId, address, count))) + .ReturnsAsync(() => _clientReadDiscreteInputsResponse); + _clientMock + .Setup(m => m.ReadHoldingRegistersAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((unitId, address, count, _) => _clientReadCallbacks.Add((unitId, address, count))) + .ReturnsAsync(() => _clientReadHoldingRegistersResponse); + _clientMock + .Setup(m => m.ReadInputRegistersAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((unitId, address, count, _) => _clientReadCallbacks.Add((unitId, address, count))) + .ReturnsAsync(() => _clientReadInputRegistersResponse); + _clientMock + .Setup(m => m.ReadDeviceIdentificationAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((unitId, category, objectId, _) => _clientReadDeviceCallbacks.Add((unitId, category, objectId))) + .ReturnsAsync(() => _clientDeviceIdentificationResponse); + + #endregion Read functions + + #region Write functions + + _clientMock + .Setup(m => m.WriteSingleCoilAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((unitId, coil, _) => _writeSingleCoilCallbacks.Add((unitId, coil))) + .ReturnsAsync(() => _clientWriteResponse); + _clientMock + .Setup(m => m.WriteSingleHoldingRegisterAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((unitId, register, _) => _writeSingleRegisterCallbacks.Add((unitId, register))) + .ReturnsAsync(() => _clientWriteResponse); + _clientMock + .Setup(m => m.WriteMultipleCoilsAsync(It.IsAny(), It.IsAny>(), It.IsAny())) + .Callback, CancellationToken>((unitId, coils, _) => _writeMultipleCoilsCallbacks.Add((unitId, coils.ToList()))) + .ReturnsAsync(() => _clientWriteResponse); + _clientMock + .Setup(m => m.WriteMultipleHoldingRegistersAsync(It.IsAny(), It.IsAny>(), It.IsAny())) + .Callback, CancellationToken>((unitId, registers, _) => _writeMultipleRegistersCallbacks.Add((unitId, registers.ToList()))) + .ReturnsAsync(() => _clientWriteResponse); + + #endregion Write functions + + var proxy = new ModbusRtuProxy(_clientMock.Object, "some-port"); + var serialPortField = proxy.GetType().GetField("_serialPort", BindingFlags.NonPublic | BindingFlags.Instance); + + ((IDisposable)serialPortField.GetValue(proxy)).Dispose(); + serialPortField.SetValue(proxy, _serialPortMock.Object); + + return proxy; + } + } +} diff --git a/AMWD.Protocols.Modbus.Tests/Serial/Snapshots/ModbusRtuProxyTest/ShouldReadDeviceIdentification.snap.bin b/AMWD.Protocols.Modbus.Tests/Serial/Snapshots/ModbusRtuProxyTest/ShouldReadDeviceIdentification.snap.bin new file mode 100644 index 0000000..76fddc0 --- /dev/null +++ b/AMWD.Protocols.Modbus.Tests/Serial/Snapshots/ModbusRtuProxyTest/ShouldReadDeviceIdentification.snap.bin @@ -0,0 +1,9 @@ +DeviceIdentification + VendorName: VendorName + ProductCode: ProductCode + MajorMinorRevision: MajorMinorRevision + VendorUrl: + ProductName: + ModelName: + UserApplicationName: + IsIndividualAccessAllowed: False