From e7300bfbde0fda370dd3aed30066ac0c07eb51a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Tue, 28 Jan 2025 21:22:11 +0100 Subject: [PATCH] Implemented UnitTests for TCP proxy --- AMWD.Protocols.Modbus.Tcp/ModbusTcpServer.cs | 1172 +-------- .../Tcp/ModbusTcpProxyTest.cs | 2220 +++++++++++++++++ CHANGELOG.md | 1 + 3 files changed, 2272 insertions(+), 1121 deletions(-) create mode 100644 AMWD.Protocols.Modbus.Tests/Tcp/ModbusTcpProxyTest.cs diff --git a/AMWD.Protocols.Modbus.Tcp/ModbusTcpServer.cs b/AMWD.Protocols.Modbus.Tcp/ModbusTcpServer.cs index 06b194f..648ead7 100644 --- a/AMWD.Protocols.Modbus.Tcp/ModbusTcpServer.cs +++ b/AMWD.Protocols.Modbus.Tcp/ModbusTcpServer.cs @@ -1,87 +1,33 @@ using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Net; -using System.Net.Sockets; -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.Tcp { /// - /// A basic implementation of a Modbus TCP server. + /// Implements a Modbus TCP server proxying all requests to a virtual Modbus client. /// [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public class ModbusTcpServer : IDisposable + public class ModbusTcpServer : ModbusTcpProxy { - #region Fields - - private bool _isDisposed; - - private TcpListener _listener; - private CancellationTokenSource _stopCts; - private Task _clientConnectTask = Task.CompletedTask; - - private readonly SemaphoreSlim _clientListLock = new(1, 1); - private readonly List _clients = []; - private readonly List _clientTasks = []; - - private readonly ReaderWriterLockSlim _deviceListLock = new(); - private readonly Dictionary _devices = []; - - private TimeSpan _readWriteTimeout = TimeSpan.FromSeconds(1); - - #endregion Fields - - #region Constructors - - /// - /// Initializes a new instance of the class. - /// - /// An to listen on (Default: ). - /// A port to listen on (Default: 502). - public ModbusTcpServer(IPAddress listenAddress = null, int listenPort = 502) + public ModbusTcpServer(IPAddress listenAddress) + : base(new VirtualModbusClient(), listenAddress) { - ListenAddress = listenAddress ?? IPAddress.Loopback; - - if (listenPort < ushort.MinValue || ushort.MaxValue < listenPort) - throw new ArgumentOutOfRangeException(nameof(listenPort)); - - try - { -#if NET8_0_OR_GREATER - using var testListener = new TcpListener(ListenAddress, listenPort); -#else - var testListener = new TcpListener(ListenAddress, listenPort); -#endif - testListener.Start(1); - ListenPort = (testListener.LocalEndpoint as IPEndPoint).Port; - testListener.Stop(); - } - catch (Exception ex) - { - throw new ArgumentException($"{nameof(ListenPort)} ({listenPort}) is already in use.", ex); - } + 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; @@ -89,1073 +35,57 @@ namespace AMWD.Protocols.Modbus.Tcp #region Properties - /// - /// Gets the to listen on. - /// - public IPAddress ListenAddress { get; } - - /// - /// Get the port to listen on. - /// - public int ListenPort { get; } - - /// - /// Gets a value indicating whether the server is running. - /// - public bool IsRunning => _listener?.Server.IsBound ?? false; - - /// - /// Gets or sets the read/write timeout. - /// - public TimeSpan ReadWriteTimeout - { - get => _readWriteTimeout; - set - { - if (value < TimeSpan.Zero) - throw new ArgumentOutOfRangeException(nameof(value)); - - _readWriteTimeout = 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(); - - _listener?.Stop(); -#if NET8_0_OR_GREATER - _listener?.Dispose(); -#endif - - _stopCts?.Dispose(); - _stopCts = new CancellationTokenSource(); - - _listener = new TcpListener(ListenAddress, ListenPort); - if (ListenAddress.AddressFamily == AddressFamily.InterNetworkV6) - _listener.Server.DualMode = true; - - _listener.Start(); - _clientConnectTask = WaitForClientAsync(_stopCts.Token); - - 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 async Task StopAsyncInternal(CancellationToken cancellationToken = default) - { - _stopCts?.Cancel(); - - _listener?.Stop(); -#if NET8_0_OR_GREATER - _listener?.Dispose(); -#endif - try - { - await Task.WhenAny(_clientConnectTask, Task.Delay(Timeout.Infinite, cancellationToken)); - } - catch (OperationCanceledException) - { - // Terminated - } - - try - { - await Task.WhenAny(Task.WhenAll(_clientTasks), Task.Delay(Timeout.Infinite, cancellationToken)); - } - catch (OperationCanceledException) - { - // Terminated - } - } - - /// - /// Releases all managed and unmanaged resources used by the . - /// - public void Dispose() - { - if (_isDisposed) - return; - - _isDisposed = true; - - StopAsyncInternal(CancellationToken.None).Wait(); - - _clientListLock.Dispose(); - _deviceListLock.Dispose(); - - _clients.Clear(); - _devices.Clear(); - - _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 async Task WaitForClientAsync(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - try - { -#if NET8_0_OR_GREATER - var client = await _listener.AcceptTcpClientAsync(cancellationToken); -#else - var client = await _listener.AcceptTcpClientAsync(); -#endif - await _clientListLock.WaitAsync(cancellationToken); - try - { - _clients.Add(client); - _clientTasks.Add(HandleClientAsync(client, cancellationToken)); - } - finally - { - _clientListLock.Release(); - } - } - catch - { - // There might be a failure here, that's ok, just keep it quiet - } - } - } - - private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken) - { - try - { - var stream = client.GetStream(); - while (!cancellationToken.IsCancellationRequested) - { - var requestBytes = new List(); - - using (var cts = new CancellationTokenSource(ReadWriteTimeout)) - using (cancellationToken.Register(cts.Cancel)) - { - byte[] headerBytes = await stream.ReadExpectedBytesAsync(6, cts.Token); - requestBytes.AddRange(headerBytes); - - byte[] followingCountBytes = headerBytes.Skip(4).Take(2).ToArray(); - followingCountBytes.SwapBigEndian(); - int followingCount = BitConverter.ToUInt16(followingCountBytes, 0); - - byte[] bodyBytes = await stream.ReadExpectedBytesAsync(followingCount, cts.Token); - requestBytes.AddRange(bodyBytes); - } - - byte[] responseBytes = HandleRequest([.. requestBytes]); - if (responseBytes != null) - await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken); - } - } - catch - { - // Keep client processing quiet - } - finally - { - await _clientListLock.WaitAsync(cancellationToken); - try - { - _clients.Remove(client); - client.Dispose(); - } - finally - { - _clientListLock.Release(); - } - } - } - - #endregion Client Handling - - #region Request Handling - - private byte[] HandleRequest(byte[] requestBytes) - { - using (_deviceListLock.GetReadLock()) - { - // No response is sent, if the device is not known - if (!_devices.TryGetValue(requestBytes[6], out var device)) - return null; - - switch ((ModbusFunctionCode)requestBytes[7]) - { - 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[9]; - Array.Copy(requestBytes, 0, responseBytes, 0, 8); - - // Mark as error - responseBytes[7] |= 0x80; - - responseBytes[8] = (byte)ModbusErrorCode.IllegalFunction; - return responseBytes; - } - } - } - } - - private static byte[] HandleReadCoils(ModbusDevice device, byte[] requestBytes) - { - if (requestBytes.Length < 12) - return null; - - var responseBytes = new List(); - responseBytes.AddRange(requestBytes.Take(8)); - - ushort firstAddress = requestBytes.GetBigEndianUInt16(8); - ushort count = requestBytes.GetBigEndianUInt16(10); - - if (count < TcpProtocol.MIN_READ_COUNT || TcpProtocol.MAX_DISCRETE_READ_COUNT < count) - { - responseBytes[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); - return [.. responseBytes]; - } - - if (firstAddress + count > ushort.MaxValue) - { - responseBytes[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress); - 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[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); - } - - return [.. responseBytes]; - } - - private static byte[] HandleReadDiscreteInputs(ModbusDevice device, byte[] requestBytes) - { - if (requestBytes.Length < 12) - return null; - - var responseBytes = new List(); - responseBytes.AddRange(requestBytes.Take(8)); - - ushort firstAddress = requestBytes.GetBigEndianUInt16(8); - ushort count = requestBytes.GetBigEndianUInt16(10); - - if (count < TcpProtocol.MIN_READ_COUNT || TcpProtocol.MAX_DISCRETE_READ_COUNT < count) - { - responseBytes[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); - return [.. responseBytes]; - } - - if (firstAddress + count > ushort.MaxValue) - { - responseBytes[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress); - 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[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); - } - - return [.. responseBytes]; - } - - private static byte[] HandleReadHoldingRegisters(ModbusDevice device, byte[] requestBytes) - { - if (requestBytes.Length < 12) - return null; - - var responseBytes = new List(); - responseBytes.AddRange(requestBytes.Take(8)); - - ushort firstAddress = requestBytes.GetBigEndianUInt16(8); - ushort count = requestBytes.GetBigEndianUInt16(10); - - if (count < TcpProtocol.MIN_READ_COUNT || TcpProtocol.MAX_REGISTER_READ_COUNT < count) - { - responseBytes[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); - return [.. responseBytes]; - } - - if (firstAddress + count > ushort.MaxValue) - { - responseBytes[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress); - 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[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); - } - - return [.. responseBytes]; - } - - private static byte[] HandleReadInputRegisters(ModbusDevice device, byte[] requestBytes) - { - if (requestBytes.Length < 12) - return null; - - var responseBytes = new List(); - responseBytes.AddRange(requestBytes.Take(8)); - - ushort firstAddress = requestBytes.GetBigEndianUInt16(8); - ushort count = requestBytes.GetBigEndianUInt16(10); - - if (count < TcpProtocol.MIN_READ_COUNT || TcpProtocol.MAX_REGISTER_READ_COUNT < count) - { - responseBytes[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); - return [.. responseBytes]; - } - - if (firstAddress + count > ushort.MaxValue) - { - responseBytes[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress); - 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[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); - } - - return [.. responseBytes]; - } - - private byte[] HandleWriteSingleCoil(ModbusDevice device, byte[] requestBytes) - { - if (requestBytes.Length < 12) - return null; - - var responseBytes = new List(); - responseBytes.AddRange(requestBytes.Take(8)); - - ushort address = requestBytes.GetBigEndianUInt16(8); - - if (requestBytes[10] != 0x00 && requestBytes[10] != 0xFF) - { - responseBytes[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); - return [.. responseBytes]; - } - - try - { - device.SetCoil(new Coil - { - Address = address, - HighByte = requestBytes[10] - }); - - // Response is an echo of the request - responseBytes.AddRange(requestBytes.Skip(8).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[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); - } - - return [.. responseBytes]; - } - - private byte[] HandleWriteSingleRegister(ModbusDevice device, byte[] requestBytes) - { - if (requestBytes.Length < 12) - return null; - - var responseBytes = new List(); - responseBytes.AddRange(requestBytes.Take(8)); - - ushort address = requestBytes.GetBigEndianUInt16(8); - ushort value = requestBytes.GetBigEndianUInt16(10); - - try - { - device.SetHoldingRegister(new HoldingRegister - { - Address = address, - HighByte = requestBytes[10], - LowByte = requestBytes[11] - }); - - // Response is an echo of the request - responseBytes.AddRange(requestBytes.Skip(8).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[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); - } - - return [.. responseBytes]; - } - - private byte[] HandleWriteMultipleCoils(ModbusDevice device, byte[] requestBytes) - { - if (requestBytes.Length < 13) - return null; - - var responseBytes = new List(); - responseBytes.AddRange(requestBytes.Take(8)); - - ushort firstAddress = requestBytes.GetBigEndianUInt16(8); - ushort count = requestBytes.GetBigEndianUInt16(10); - - int byteCount = (int)Math.Ceiling(count / 8.0); - if (requestBytes.Length < 13 + byteCount) - { - responseBytes[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); - return [.. responseBytes]; - } - - try - { - int baseOffset = 13; - 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(8).Take(4)); - } - catch - { - responseBytes[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); - } - - return [.. responseBytes]; - } - - private byte[] HandleWriteMultipleRegisters(ModbusDevice device, byte[] requestBytes) - { - if (requestBytes.Length < 13) - return null; - - var responseBytes = new List(); - responseBytes.AddRange(requestBytes.Take(8)); - - ushort firstAddress = requestBytes.GetBigEndianUInt16(8); - ushort count = requestBytes.GetBigEndianUInt16(10); - - int byteCount = count * 2; - if (requestBytes.Length < 13 + byteCount) - { - responseBytes[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); - return [.. responseBytes]; - } - - try - { - int baseOffset = 13; - 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(8).Take(4)); - } - catch - { - responseBytes[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); - } - - return [.. responseBytes]; - } - - private byte[] HandleEncapsulatedInterface(byte[] requestBytes) - { - var responseBytes = new List(); - responseBytes.AddRange(requestBytes.Take(8)); - - if (requestBytes[8] != 0x0E) - { - responseBytes[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalFunction); - return [.. responseBytes]; - } - - if (0x06 < requestBytes[10] && requestBytes[10] < 0x80) - { - responseBytes[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress); - return [.. responseBytes]; - } - - var category = (ModbusDeviceIdentificationCategory)requestBytes[9]; - if (!Enum.IsDefined(typeof(ModbusDeviceIdentificationCategory), category)) - { - responseBytes[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); - return [.. responseBytes]; - } - - try - { - var bodyBytes = new List(); - // MEI, Category - bodyBytes.AddRange(requestBytes.Skip(8).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[10] < 0x03) - bodyBytes[2] = 0x81; - else if (requestBytes[10] < 0x80) - bodyBytes[2] = 0x82; - else - bodyBytes[2] = 0x83; - - maxObjectId = requestBytes[10]; - } - - break; - } - - byte numberOfObjects = 0; - for (int i = requestBytes[10]; 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 > TcpProtocol.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); - return [.. responseBytes]; - } - catch - { - responseBytes[7] |= 0x80; - responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); - 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-TCP"); - 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("TCP Server"); - result.Add((byte)bytes.Length); - result.AddRange(bytes); - } - break; - - case ModbusDeviceIdentificationObject.UserApplicationName: - { - byte[] bytes = Encoding.UTF8.GetBytes("Modbus TCP Server"); - result.Add((byte)bytes.Length); - result.AddRange(bytes); - } - break; - - default: - result.Add(0x00); - break; - } - - return [.. result]; - } - - #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.Tests/Tcp/ModbusTcpProxyTest.cs b/AMWD.Protocols.Modbus.Tests/Tcp/ModbusTcpProxyTest.cs new file mode 100644 index 0000000..07ef5ac --- /dev/null +++ b/AMWD.Protocols.Modbus.Tests/Tcp/ModbusTcpProxyTest.cs @@ -0,0 +1,2220 @@ +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using AMWD.Protocols.Modbus.Tcp; +using AMWD.Protocols.Modbus.Tcp.Utils; +using Moq; + +namespace AMWD.Protocols.Modbus.Tests.Tcp +{ + [TestClass] + public class ModbusTcpProxyTest + { + private bool _connectClient; + + private Mock _clientMock; + private Mock _tcpListenerMock; + private Mock _socketMock; + private Mock _ipEndPointMock; + private Mock _tcpClientMock; + private Mock _networkStreamMock; + + private bool _socketBound; + + 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() + { + _connectClient = true; + + _socketBound = false; + + _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 + _connectClient = false; + + // Act + using (var proxy = GetProxy()) + { + // Assert + Assert.IsNotNull(proxy); + } + + // Assert + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.Dispose(), Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public void ShouldDispose() + { + // Arrange + _connectClient = false; + + // Act + using (var proxy = GetProxy()) + { + proxy.Dispose(); + } + + // Assert + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.Dispose(), Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldStartAndStop() + { + // Arrange + _connectClient = false; + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await proxy.StopAsync(); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Exactly(2)); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.Once); + + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void ShouldThrowArgumentNullExceptionOnCreateInstanceForClient() + { + // Arrange + + // Act + new ModbusTcpProxy(null, IPAddress.Loopback); + + // Assert - ArgumentNullException + } + + [TestMethod] + public void ShouldGetAllProperties() + { + // Arrange + _connectClient = false; + using var proxy = GetProxy(); + + // Act + Assert.AreEqual(IPAddress.Loopback, proxy.ListenAddress); + Assert.AreEqual(502, proxy.ListenPort); + Assert.IsFalse(proxy.IsRunning); + Assert.AreEqual(100, proxy.ReadWriteTimeout.TotalSeconds); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Exactly(2)); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Port, Times.Once); + _socketMock.VerifyGet(m => m.IsBound, Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public void ShouldSetAllProperties() + { + // Arrange + _connectClient = false; + using var proxy = GetProxy(); + + // Act + proxy.ListenAddress = IPAddress.Any; + proxy.ListenPort = 55033; + proxy.ReadWriteTimeout = TimeSpan.FromSeconds(3); + + // Assert + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Exactly(2)); + _ipEndPointMock.VerifySet(m => m.Address = IPAddress.Any, Times.Once); + _ipEndPointMock.VerifySet(m => m.Port = 55033, Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ShouldThrowArgumentOutOfRangeExceptionForInvalidTimeout() + { + // Arrange + _connectClient = false; + using var proxy = GetProxy(); + + // Act + proxy.ReadWriteTimeout = TimeSpan.FromSeconds(-3); + + // Assert - ArgumentOutOfRangeException + } + + [TestMethod] + public async Task ShouldIgnoreExceptionInWaitForClient() + { + // Arrange + using var proxy = GetProxy(); + _tcpListenerMock + .Setup(m => m.AcceptTcpClientAsync(It.IsAny())) + .Returns(async (ct) => + { + await Task.Run(() => SpinWait.SpinUntil(() => _connectClient || ct.IsCancellationRequested)); + _connectClient = false; + throw new Exception(); + }); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldIgnoreExceptionInHandleClientAsync() + { + // Arrange + using var proxy = GetProxy(); + _tcpClientMock.Setup(m => m.GetStream()).Throws(new Exception()); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + _tcpClientMock.Verify(m => m.Dispose(), Times.Once); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldReturnIllegalFunction() + { + // Arrange + byte[] request = [1, 14, 0, 5, 0, 4]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([1, 142, 1]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse, _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(CreateHeader(request)); + _requestBytesQueue.Enqueue(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 = CreateMessage([2, 1, 1, 9]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.ReadCoilsAsync(2, 5, 4, It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), 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, _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldIgnoreTooShortRequestOnReadCoils() + { + // Arrange + byte[] request = [2, 1, 0, 5, 4]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnReadCoils() + { + // Arrange + byte[] request = [2, 1, 0, 5, 0, 4]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([2, 129, 4]); + + using var proxy = GetProxy(); + + _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 + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.ReadCoilsAsync(2, 5, 4, It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), 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, _responseBytesCallbacks.First()); + } + + #endregion Read Coils (Fn 1) + + #region Read Discrete Inputs (Fn 2) + + [TestMethod] + public async Task ShouldReadDiscreteInputs() + { + // Arrange + byte[] request = [2, 2, 0, 5, 0, 4]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(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 = CreateMessage([2, 2, 1, 10]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.ReadDiscreteInputsAsync(2, 5, 4, It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), 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, _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldIgnoreTooShortRequestOnReadDiscreteInputs() + { + // Arrange + byte[] request = [2, 2, 0, 5, 4]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnReadDiscreteInputs() + { + // Arrange + byte[] request = [2, 2, 0, 5, 0, 4]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([2, 130, 4]); + + using var proxy = GetProxy(); + + _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 + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.ReadDiscreteInputsAsync(2, 5, 4, It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), 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, _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(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + _clientReadHoldingRegistersResponse = [ + new HoldingRegister { Address = 15, LowByte = 12, HighByte = 34 }, + new HoldingRegister { Address = 16, LowByte = 56, HighByte = 78 }, + ]; + byte[] expectedResponse = CreateMessage([42, 3, 4, 34, 12, 78, 56]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.ReadHoldingRegistersAsync(42, 15, 2, It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), 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, _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldIgnoreTooShortRequestOnReadHoldingRegisters() + { + // Arrange + byte[] request = [42, 3, 0, 15, 2]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnReadHoldingRegisters() + { + // Arrange + byte[] request = [42, 3, 0, 15, 0, 2]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([42, 131, 4]); + + using var proxy = GetProxy(); + + _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 + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.ReadHoldingRegistersAsync(42, 15, 2, It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), 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, _responseBytesCallbacks.First()); + } + + #endregion Read Holding Registers (Fn 3) + + #region Read Input Registers (Fn 4) + + [TestMethod] + public async Task ShouldReadInputRegisters() + { + // Arrange + byte[] request = [24, 4, 0, 10, 0, 2]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + _clientReadInputRegistersResponse = [ + new InputRegister { Address = 10, LowByte = 34, HighByte = 12 }, + new InputRegister { Address = 11, LowByte = 78, HighByte = 56 }, + ]; + byte[] expectedResponse = CreateMessage([24, 4, 4, 12, 34, 56, 78]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.ReadInputRegistersAsync(24, 10, 2, It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + var (unitId, startAddress, count) = _clientReadCallbacks.First(); + Assert.AreEqual(24, unitId); + Assert.AreEqual(10, startAddress); + Assert.AreEqual(2, count); + + CollectionAssert.AreEqual(expectedResponse, _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldIgnoreTooShortRequestOnReadInputRegisters() + { + // Arrange + byte[] request = [24, 4, 0, 10, 2]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnReadInputRegisters() + { + // Arrange + byte[] request = [24, 4, 0, 10, 0, 2]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([24, 132, 4]); + + using var proxy = GetProxy(); + + _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 + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.ReadInputRegistersAsync(24, 10, 2, It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + var (unitId, startAddress, count) = _clientReadCallbacks.First(); + Assert.AreEqual(24, unitId); + Assert.AreEqual(10, startAddress); + Assert.AreEqual(2, count); + + CollectionAssert.AreEqual(expectedResponse, _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(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([ + 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(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.ReadDeviceIdentificationAsync(1, ModbusDeviceIdentificationCategory.Basic, ModbusDeviceIdentificationObject.VendorName, It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), 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, _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldIgnoreTooShortRequestOnReadDeviceIdentification() + { + // Arrange + byte[] request = [1, 43, 14, 1]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldReturnIllegalFunctionForWrongTypeOnReadDeviceIdentification() + { + // Arrange + byte[] request = [1, 43, 13, 1, 0]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([1, 171, 1]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse, _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReturnIllegalDataAddressForWrongTypeOnReadDeviceIdentification() + { + // Arrange + byte[] request = [1, 43, 14, 4, 10]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([1, 171, 2]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse, _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReturnIllegalDataValueForWrongTypeOnReadDeviceIdentification() + { + // Arrange + byte[] request = [1, 43, 14, 0, 0]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([1, 171, 3]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse, _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReadDeviceIdentificationWithIndividualAccessAllowed() + { + // Arrange + byte[] request = [1, 43, 14, 1, 0]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + _clientDeviceIdentificationResponse.IsIndividualAccessAllowed = true; + byte[] expectedResponse = CreateMessage([ + 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(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.ReadDeviceIdentificationAsync(1, ModbusDeviceIdentificationCategory.Basic, ModbusDeviceIdentificationObject.VendorName, It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), 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, _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReadDeviceIdentificationRegular() + { + // Arrange + byte[] request = [1, 43, 14, 2, 0]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([ + 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(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.ReadDeviceIdentificationAsync(1, ModbusDeviceIdentificationCategory.Regular, ModbusDeviceIdentificationObject.VendorName, It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), 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, _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReadDeviceIdentificationExtended() + { + // Arrange + byte[] request = [1, 43, 14, 3, 0]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([ + 1, 43, 14, 3, + 3, 255, 223, 102, + 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]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.ReadDeviceIdentificationAsync(1, ModbusDeviceIdentificationCategory.Extended, ModbusDeviceIdentificationObject.VendorName, It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), 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, _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReadDeviceIdentificationIndividual() + { + // Arrange + byte[] request = [1, 43, 14, 4, 0]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + _clientDeviceIdentificationResponse.IsIndividualAccessAllowed = true; + byte[] expectedResponse = CreateMessage([ + 1, 43, 14, 4, + 132, 0, 0, 1, + 0, 10, 86, 101, 110, 100, 111, 114, 78, 97, 109, 101]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.ReadDeviceIdentificationAsync(1, ModbusDeviceIdentificationCategory.Individual, ModbusDeviceIdentificationObject.VendorName, It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), 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, _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureForWrongTypeOnReadDeviceIdentification() + { + // Arrange + byte[] request = [1, 43, 14, 1, 0]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([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()); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.ReadDeviceIdentificationAsync(1, ModbusDeviceIdentificationCategory.Basic, ModbusDeviceIdentificationObject.VendorName, It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), 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, _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(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([3, 5, 0, 7, 255, 0]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.WriteSingleCoilAsync(3, It.IsAny(), It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), 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, _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldIgnoreTooShortRequestOnWriteSingleCoil() + { + // Arrange + byte[] request = [3, 5, 0, 7, 255]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldReturnIllegalDataValueOnWriteSingleCoil() + { + // Arrange + byte[] request = [3, 5, 0, 7, 250, 0]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([3, 133, 3]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse, _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnWriteSingleCoilForNotSuccessful() + { + // Arrange + _clientWriteResponse = false; + byte[] request = [3, 5, 0, 7, 255, 0]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([3, 133, 4]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.WriteSingleCoilAsync(3, It.IsAny(), It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), 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, _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnWriteSingleCoilForException() + { + // Arrange + byte[] request = [3, 5, 0, 7, 255, 0]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([3, 133, 4]); + + using var proxy = GetProxy(); + + _clientMock + .Setup(m => m.WriteSingleCoilAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((unitId, coil, _) => _writeSingleCoilCallbacks.Add((unitId, coil))) + .ThrowsAsync(new ModbusException()); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.WriteSingleCoilAsync(3, It.IsAny(), It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), 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, _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(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([4, 6, 0, 1, 0, 3]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.WriteSingleHoldingRegisterAsync(4, It.IsAny(), It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), 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, _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldIgnoreTooShortRequestOnWriteSingleRegister() + { + // Arrange + byte[] request = [4, 6, 0, 1, 3]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnWriteSingleRegisterForNotSuccessful() + { + // Arrange + _clientWriteResponse = false; + byte[] request = [4, 6, 0, 1, 0, 3]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([4, 134, 4]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.WriteSingleHoldingRegisterAsync(4, It.IsAny(), It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), 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, _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnWriteSingleRegisterForException() + { + // Arrange + byte[] request = [4, 6, 0, 1, 0, 3]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([4, 134, 4]); + + using var proxy = GetProxy(); + + _clientMock + .Setup(m => m.WriteSingleHoldingRegisterAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((unitId, register, _) => _writeSingleRegisterCallbacks.Add((unitId, register))) + .ThrowsAsync(new ModbusException()); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.WriteSingleHoldingRegisterAsync(4, It.IsAny(), It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), 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, _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(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([1, 15, 0, 13, 0, 10]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.WriteMultipleCoilsAsync(1, It.IsAny>(), It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse, _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(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldReturnIllegalDataValueOnWriteMultipleCoils() + { + // Arrange + byte[] request = [1, 15, 0, 13, 0, 10, 2, 205]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([1, 143, 3]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse, _responseBytesCallbacks.First()); + } + + [TestMethod] + public async Task ShouldReturnSlaveDeviceFailureOnWriteMultipleCoilsForNotSuccessful() + { + // Arrange + _clientWriteResponse = false; + byte[] request = [1, 15, 0, 13, 0, 10, 2, 205, 1]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([1, 143, 4]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.WriteMultipleCoilsAsync(1, It.IsAny>(), It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse, _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(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([1, 143, 4]); + + using var proxy = GetProxy(); + + _clientMock + .Setup(m => m.WriteMultipleCoilsAsync(It.IsAny(), It.IsAny>(), It.IsAny())) + .Callback, CancellationToken>((unitId, coils, _) => _writeMultipleCoilsCallbacks.Add((unitId, coils.ToList()))) + .ThrowsAsync(new ModbusException()); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.WriteMultipleCoilsAsync(1, It.IsAny>(), It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse, _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(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([1, 16, 0, 1, 0, 2]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.WriteMultipleHoldingRegistersAsync(1, It.IsAny>(), It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse, _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(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldReturnIllegalDataValueOnWriteMultipleRegisters() + { + // Arrange + byte[] request = [1, 16, 0, 1, 0, 2, 4, 0, 10, 1]; + _requestBytesQueue.Enqueue(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([1, 144, 3]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse, _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(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([1, 144, 4]); + + using var proxy = GetProxy(); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.WriteMultipleHoldingRegistersAsync(1, It.IsAny>(), It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse, _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(CreateHeader(request)); + _requestBytesQueue.Enqueue(request); + byte[] expectedResponse = CreateMessage([1, 144, 4]); + + using var proxy = GetProxy(); + + _clientMock + .Setup(m => m.WriteMultipleHoldingRegistersAsync(It.IsAny(), It.IsAny>(), It.IsAny())) + .Callback, CancellationToken>((unitId, coils, _) => _writeMultipleRegistersCallbacks.Add((unitId, coils.ToList()))) + .ThrowsAsync(new ModbusException()); + + // Act + await proxy.StartAsync(); + await Task.Delay(100); + + // Assert + _tcpListenerMock.VerifyGet(m => m.Socket, Times.Once); + _tcpListenerMock.VerifyGet(m => m.LocalIPEndPoint, Times.Once); + _ipEndPointMock.VerifyGet(m => m.Address, Times.Once); + _socketMock.VerifySet(m => m.DualMode = false, Times.Once); + + _tcpListenerMock.Verify(m => m.Start(), Times.Once); + _tcpListenerMock.Verify(m => m.Stop(), Times.Once); + _tcpListenerMock.Verify(m => m.AcceptTcpClientAsync(It.IsAny()), Times.AtLeast(1)); + + _tcpClientMock.Verify(m => m.GetStream(), Times.Once); + + _clientMock.Verify(m => m.WriteMultipleHoldingRegistersAsync(1, It.IsAny>(), It.IsAny()), Times.Once); + + _networkStreamMock.Verify(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _networkStreamMock.Verify(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + VerifyNoOtherCalls(); + + CollectionAssert.AreEqual(expectedResponse, _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(); + _tcpListenerMock.VerifyNoOtherCalls(); + _ipEndPointMock.VerifyNoOtherCalls(); + _socketMock.VerifyNoOtherCalls(); + _tcpClientMock.VerifyNoOtherCalls(); + _networkStreamMock.VerifyNoOtherCalls(); + } + + private static byte[] CreateHeader(IReadOnlyList request) + { + ushort length = (ushort)request.Count; + byte[] bytes = BitConverter.GetBytes(length); + if (BitConverter.IsLittleEndian) + Array.Reverse(bytes); + + return [0, 1, 0, 0, .. bytes]; + } + + private static byte[] CreateMessage(IReadOnlyList request) + { + return [.. CreateHeader(request), .. request]; + } + + private ModbusTcpProxy GetProxy() + { + var localAddress = IPAddress.Loopback; + int localPort = 502; + + var connection = new Mock(); + + _clientMock = new Mock(connection.Object); + _tcpListenerMock = new Mock(localAddress, localPort); + _ipEndPointMock = new Mock(null); + _socketMock = new Mock(null); + _tcpClientMock = new Mock(AddressFamily.InterNetwork); + _networkStreamMock = new Mock(null); + + #region General + + _tcpListenerMock + .Setup(m => m.Socket) + .Returns(() => _socketMock.Object); + _tcpListenerMock + .Setup(m => m.LocalIPEndPoint) + .Returns(() => _ipEndPointMock.Object); + _tcpListenerMock + .Setup(m => m.AcceptTcpClientAsync(It.IsAny())) + .Returns(async (ct) => + { + await Task.Run(() => SpinWait.SpinUntil(() => _connectClient || ct.IsCancellationRequested)); + ct.ThrowIfCancellationRequested(); + _connectClient = false; + + return _tcpClientMock.Object; + }); + + _ipEndPointMock.SetupProperty(m => m.Address, localAddress); + _ipEndPointMock.SetupProperty(m => m.Port, localPort); + + _socketMock.SetupProperty(m => m.DualMode, false); + _socketMock.SetupGet(m => m.IsBound).Returns(() => _socketBound); + + _tcpClientMock + .Setup(m => m.GetStream()) + .Returns(() => _networkStreamMock.Object); + + _networkStreamMock + .Setup(m => m.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(async (buffer, offset, count, ct) => + { + await Task.Run(() => SpinWait.SpinUntil(() => _requestBytesQueue.Count > 0 || ct.IsCancellationRequested)); + ct.ThrowIfCancellationRequested(); + + byte[] bytes = _requestBytesQueue.Dequeue(); + int minLength = Math.Min(bytes.Length, count); + + Array.Copy(bytes, 0, buffer, offset, minLength); + return minLength; + }); + _networkStreamMock + .Setup(m => m.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((buffer, _, __, ___) => _responseBytesCallbacks.Add(buffer)) + .Returns(Task.CompletedTask); + + #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 ModbusTcpProxy(_clientMock.Object, localAddress); + var tcpListenerField = proxy.GetType().GetField("_tcpListener", BindingFlags.NonPublic | BindingFlags.Instance); + + ((IDisposable)tcpListenerField.GetValue(proxy)).Dispose(); + tcpListenerField.SetValue(proxy, _tcpListenerMock.Object); + + return proxy; + } + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index f506e97..89ec91b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `ModbusTcpProxy.ReadWriteTimeout` has a default value of 100 seconds (same default as a `HttpClient` has). - The `ModbusRtuProxy` moved from `AMWD.Protocols.Modbus.Proxy` to `AMWD.Protocols.Modbus.Serial`. - The `ModbusTcpProxy` moved from `AMWD.Protocols.Modbus.Proxy` to `AMWD.Protocols.Modbus.Tcp`. +- Server implementations are proxies with a virtual Modbus client. ### Removed