diff --git a/AMWD.Protocols.Modbus.Common/InternalsVisibleTo.cs b/AMWD.Protocols.Modbus.Common/InternalsVisibleTo.cs
index c89850e..82a8ede 100644
--- a/AMWD.Protocols.Modbus.Common/InternalsVisibleTo.cs
+++ b/AMWD.Protocols.Modbus.Common/InternalsVisibleTo.cs
@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("AMWD.Protocols.Modbus.Tests")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
diff --git a/AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs b/AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs
index 868f8c8..c87480c 100644
--- a/AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs
+++ b/AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs
@@ -59,6 +59,15 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
///
public const ushort MAX_REGISTER_WRITE_COUNT = 0x007B; // 123
+ ///
+ /// The maximum allowed ADU length in bytes.
+ ///
+ ///
+ /// A Modbus frame consists of a PDU (protcol data unit) and additional protocol addressing / error checks.
+ /// The whole data frame is called ADU (application data unit).
+ ///
+ public const int MAX_ADU_LENGTH = 260; // bytes
+
#endregion Constants
///
diff --git a/AMWD.Protocols.Modbus.Tcp/AMWD.Protocols.Modbus.Tcp.csproj b/AMWD.Protocols.Modbus.Tcp/AMWD.Protocols.Modbus.Tcp.csproj
index e8b7175..15fedd4 100644
--- a/AMWD.Protocols.Modbus.Tcp/AMWD.Protocols.Modbus.Tcp.csproj
+++ b/AMWD.Protocols.Modbus.Tcp/AMWD.Protocols.Modbus.Tcp.csproj
@@ -13,6 +13,12 @@
Modbus Protocol Network TCP LAN
+
+
+
+
+
+
diff --git a/AMWD.Protocols.Modbus.Tcp/Events/CoilWrittenEventArgs.cs b/AMWD.Protocols.Modbus.Tcp/Events/CoilWrittenEventArgs.cs
new file mode 100644
index 0000000..256299f
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Tcp/Events/CoilWrittenEventArgs.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace AMWD.Protocols.Modbus.Tcp.Events
+{
+ ///
+ /// Represents the coil written event arguments.
+ ///
+ public class CoilWrittenEventArgs : EventArgs
+ {
+ ///
+ /// Gets or sets the unit id.
+ ///
+ public byte UnitId { get; internal set; }
+
+ ///
+ /// Gets or sets the coil address.
+ ///
+ public ushort Address { get; internal set; }
+
+ ///
+ /// Gets or sets the coil value.
+ ///
+ public bool Value { get; internal set; }
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Tcp/Events/RegisterWrittenEventArgs.cs b/AMWD.Protocols.Modbus.Tcp/Events/RegisterWrittenEventArgs.cs
new file mode 100644
index 0000000..fe5da73
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Tcp/Events/RegisterWrittenEventArgs.cs
@@ -0,0 +1,35 @@
+using System;
+
+namespace AMWD.Protocols.Modbus.Tcp.Events
+{
+ ///
+ /// Represents the register written event arguments.
+ ///
+ public class RegisterWrittenEventArgs : EventArgs
+ {
+ ///
+ /// Gets or sets the unit id.
+ ///
+ public byte UnitId { get; internal set; }
+
+ ///
+ /// Gets or sets the address of the register.
+ ///
+ public ushort Address { get; internal set; }
+
+ ///
+ /// Gets or sets the value of the register.
+ ///
+ public ushort Value { get; internal set; }
+
+ ///
+ /// Gets or sets the high byte of the register.
+ ///
+ public byte HighByte { get; internal set; }
+
+ ///
+ /// Gets or sets the low byte of the register.
+ ///
+ public byte LowByte { get; internal set; }
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Tcp/Extensions/StreamExtensions.cs b/AMWD.Protocols.Modbus.Tcp/Extensions/StreamExtensions.cs
new file mode 100644
index 0000000..545ce63
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Tcp/Extensions/StreamExtensions.cs
@@ -0,0 +1,26 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.IO
+{
+ internal static class StreamExtensions
+ {
+ public static async Task ReadExpectedBytesAsync(this Stream stream, int expectedBytes, CancellationToken cancellationToken = default)
+ {
+ byte[] buffer = new byte[expectedBytes];
+ int offset = 0;
+ do
+ {
+ int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken).ConfigureAwait(false);
+ if (count < 1)
+ throw new EndOfStreamException();
+
+ offset += count;
+ }
+ while (offset < expectedBytes && !cancellationToken.IsCancellationRequested);
+
+ cancellationToken.ThrowIfCancellationRequested();
+ return buffer;
+ }
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Tcp/InternalsVisibleTo.cs b/AMWD.Protocols.Modbus.Tcp/InternalsVisibleTo.cs
deleted file mode 100644
index 82a8ede..0000000
--- a/AMWD.Protocols.Modbus.Tcp/InternalsVisibleTo.cs
+++ /dev/null
@@ -1,4 +0,0 @@
-using System.Runtime.CompilerServices;
-
-[assembly: InternalsVisibleTo("AMWD.Protocols.Modbus.Tests")]
-[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
diff --git a/AMWD.Protocols.Modbus.Tcp/ModbusTcpServer.cs b/AMWD.Protocols.Modbus.Tcp/ModbusTcpServer.cs
new file mode 100644
index 0000000..8cc66a1
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Tcp/ModbusTcpServer.cs
@@ -0,0 +1,1149 @@
+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.Models;
+using AMWD.Protocols.Modbus.Common.Protocols;
+using AMWD.Protocols.Modbus.Tcp.Events;
+
+namespace AMWD.Protocols.Modbus.Tcp
+{
+ ///
+ /// A basic implementation of a Modbus TCP server.
+ ///
+ [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public class ModbusTcpServer : IDisposable
+ {
+ #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 = [];
+
+ #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)
+ {
+ ListenAddress = listenAddress ?? IPAddress.Loopback;
+
+ if (ushort.MinValue < listenPort || listenPort < ushort.MaxValue)
+ 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);
+ }
+ }
+
+ #endregion Constructors
+
+ #region Events
+
+ ///
+ /// Occurs when a is written.
+ ///
+ public event EventHandler CoilWritten;
+
+ ///
+ /// Occurs when a is written.
+ ///
+ public event EventHandler RegisterWritten;
+
+ #endregion Events
+
+ #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; set; }
+
+ #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();
+ }
+
+ 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).ConfigureAwait(false);
+#else
+ var client = await _listener.AcceptTcpClientAsync().ConfigureAwait(false);
+#endif
+ await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+ 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).ConfigureAwait(false);
+ requestBytes.AddRange(headerBytes);
+
+ byte[] followingCountBytes = headerBytes.Skip(4).Take(2).ToArray();
+ followingCountBytes.SwapNetworkOrder();
+ int followingCount = BitConverter.ToUInt16(followingCountBytes, 0);
+
+ byte[] bodyBytes = await stream.ReadExpectedBytesAsync(followingCount, cts.Token).ConfigureAwait(false);
+ requestBytes.AddRange(bodyBytes);
+ }
+
+ byte[] responseBytes = HandleRequest([.. requestBytes]);
+ if (responseBytes != null)
+ await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ catch
+ {
+ // Keep client processing quiet
+ }
+ finally
+ {
+ await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+ 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.NetworkUInt16(8);
+ ushort count = requestBytes.NetworkUInt16(10);
+
+ if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_DISCRETE_READ_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.NetworkUInt16(8);
+ ushort count = requestBytes.NetworkUInt16(10);
+
+ if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_DISCRETE_READ_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.NetworkUInt16(8);
+ ushort count = requestBytes.NetworkUInt16(10);
+
+ if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_REGISTER_READ_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.NetworkUInt16(8);
+ ushort count = requestBytes.NetworkUInt16(10);
+
+ if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_REGISTER_READ_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.NetworkUInt16(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.NetworkUInt16(8);
+ ushort value = requestBytes.NetworkUInt16(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.NetworkUInt16(8);
+ ushort count = requestBytes.NetworkUInt16(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.NetworkUInt16(8);
+ ushort count = requestBytes.NetworkUInt16(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.NetworkUInt16(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();
+
+ 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);
+ }
+ }
+
+ #endregion Device Handling
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Tests/Tcp/Utils/ModbusTcpConnectionTest.cs b/AMWD.Protocols.Modbus.Tests/Tcp/Utils/ModbusTcpConnectionTest.cs
index 690205f..4ca5dda 100644
--- a/AMWD.Protocols.Modbus.Tests/Tcp/Utils/ModbusTcpConnectionTest.cs
+++ b/AMWD.Protocols.Modbus.Tests/Tcp/Utils/ModbusTcpConnectionTest.cs
@@ -161,8 +161,8 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp.Utils
public async Task ShouldThrowApplicationExceptionHostnameNotResolvable()
{
// Arrange
- _hostname = "123.321.123.321";
var connection = GetConnection();
+ connection.GetType().GetField("_hostname", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(connection, "");
// Act
await connection.ConnectAsync();