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