diff --git a/AMWD.Protocols.Modbus.Proxy/AMWD.Protocols.Modbus.Proxy.csproj b/AMWD.Protocols.Modbus.Proxy/AMWD.Protocols.Modbus.Proxy.csproj
new file mode 100644
index 0000000..5d6b82e
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Proxy/AMWD.Protocols.Modbus.Proxy.csproj
@@ -0,0 +1,30 @@
+
+
+
+ netstandard2.0;net6.0;net8.0
+ 12.0
+
+ AMWD.Protocols.Modbus.Proxy
+ amwd-modbus-proxy
+ AMWD.Protocols.Modbus.Proxy
+
+ Modbus Proxy Clients
+ Plugging Modbus Servers and Clients together to create Modbus Proxies.
+ Modbus Protocol Proxy
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AMWD.Protocols.Modbus.Proxy/ModbusTcpProxy.cs b/AMWD.Protocols.Modbus.Proxy/ModbusTcpProxy.cs
new file mode 100644
index 0000000..f94fc66
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Proxy/ModbusTcpProxy.cs
@@ -0,0 +1,853 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using AMWD.Protocols.Modbus.Common;
+using AMWD.Protocols.Modbus.Common.Contracts;
+using AMWD.Protocols.Modbus.Common.Protocols;
+
+namespace AMWD.Protocols.Modbus.Proxy
+{
+ ///
+ /// Implements a Modbus TCP server proxying all requests to a Modbus client of choice.
+ ///
+ public class ModbusTcpProxy : 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 = [];
+
+ #endregion Fields
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The used to request the remote device, that should be proxied.
+ /// An to listen on (Default: ).
+ /// A port to listen on (Default: 502).
+ public ModbusTcpProxy(ModbusClientBase client, IPAddress listenAddress = null, int listenPort = 502)
+ {
+ Client = client ?? throw new ArgumentNullException(nameof(client));
+
+ 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 Properties
+
+ ///
+ /// Gets the Modbus client used to request the remote device, that should be proxied.
+ ///
+ public ModbusClientBase Client { get; }
+
+ ///
+ /// 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 for the incoming connections (not the !).
+ ///
+ 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();
+ _clients.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.SwapBigEndian();
+ int followingCount = BitConverter.ToUInt16(followingCountBytes, 0);
+
+ byte[] bodyBytes = await stream.ReadExpectedBytesAsync(followingCount, cts.Token).ConfigureAwait(false);
+ requestBytes.AddRange(bodyBytes);
+ }
+
+ byte[] responseBytes = await HandleRequestAsync([.. requestBytes], cancellationToken).ConfigureAwait(false);
+ 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 Task HandleRequestAsync(byte[] requestBytes, CancellationToken cancellationToken)
+ {
+ switch ((ModbusFunctionCode)requestBytes[7])
+ {
+ case ModbusFunctionCode.ReadCoils:
+ return HandleReadCoilsAsync(requestBytes, cancellationToken);
+
+ case ModbusFunctionCode.ReadDiscreteInputs:
+ return HandleReadDiscreteInputsAsync(requestBytes, cancellationToken);
+
+ case ModbusFunctionCode.ReadHoldingRegisters:
+ return HandleReadHoldingRegistersAsync(requestBytes, cancellationToken);
+
+ case ModbusFunctionCode.ReadInputRegisters:
+ return HandleReadInputRegistersAsync(requestBytes, cancellationToken);
+
+ case ModbusFunctionCode.WriteSingleCoil:
+ return HandleWriteSingleCoilAsync(requestBytes, cancellationToken);
+
+ case ModbusFunctionCode.WriteSingleRegister:
+ return HandleWriteSingleRegisterAsync(requestBytes, cancellationToken);
+
+ case ModbusFunctionCode.WriteMultipleCoils:
+ return HandleWriteMultipleCoilsAsync(requestBytes, cancellationToken);
+
+ case ModbusFunctionCode.WriteMultipleRegisters:
+ return HandleWriteMultipleRegistersAsync(requestBytes, cancellationToken);
+
+ case ModbusFunctionCode.EncapsulatedInterface:
+ return HandleEncapsulatedInterfaceAsync(requestBytes, cancellationToken);
+
+ 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 Task.FromResult(responseBytes);
+ }
+ }
+ }
+
+ private async Task HandleReadCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
+ {
+ if (requestBytes.Length < 12)
+ return null;
+
+ byte unitId = requestBytes[6];
+ ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
+ ushort count = requestBytes.GetBigEndianUInt16(10);
+
+ var responseBytes = new List();
+ responseBytes.AddRange(requestBytes.Take(8));
+ try
+ {
+ var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(false);
+
+ byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)];
+ for (int i = 0; i < coils.Count; i++)
+ {
+ if (coils[i].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 async Task HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken)
+ {
+ if (requestBytes.Length < 12)
+ return null;
+
+ byte unitId = requestBytes[6];
+ ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
+ ushort count = requestBytes.GetBigEndianUInt16(10);
+
+ var responseBytes = new List();
+ responseBytes.AddRange(requestBytes.Take(8));
+ try
+ {
+ var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(false);
+
+ byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)];
+ for (int i = 0; i < discreteInputs.Count; i++)
+ {
+ if (discreteInputs[i].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 async Task HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
+ {
+ if (requestBytes.Length < 12)
+ return null;
+
+ byte unitId = requestBytes[6];
+ ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
+ ushort count = requestBytes.GetBigEndianUInt16(10);
+
+ var responseBytes = new List();
+ responseBytes.AddRange(requestBytes.Take(8));
+ try
+ {
+ var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(false);
+
+ byte[] values = new byte[holdingRegisters.Count * 2];
+ for (int i = 0; i < holdingRegisters.Count; i++)
+ {
+ values[i * 2] = holdingRegisters[i].HighByte;
+ values[i * 2 + 1] = holdingRegisters[i].LowByte;
+ }
+
+ responseBytes.Add((byte)values.Length);
+ responseBytes.AddRange(values);
+ }
+ catch
+ {
+ responseBytes[7] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
+ }
+
+ return [.. responseBytes];
+ }
+
+ private async Task HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
+ {
+ if (requestBytes.Length < 12)
+ return null;
+
+ byte unitId = requestBytes[6];
+ ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
+ ushort count = requestBytes.GetBigEndianUInt16(10);
+
+ var responseBytes = new List();
+ responseBytes.AddRange(requestBytes.Take(8));
+ try
+ {
+ var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(false);
+
+ byte[] values = new byte[count * 2];
+ for (int i = 0; i < count; i++)
+ {
+ values[i * 2] = inputRegisters[i].HighByte;
+ values[i * 2 + 1] = inputRegisters[i].LowByte;
+ }
+
+ responseBytes.Add((byte)values.Length);
+ responseBytes.AddRange(values);
+ }
+ catch
+ {
+ responseBytes[7] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
+ }
+
+ return [.. responseBytes];
+ }
+
+ private async Task HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken)
+ {
+ 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
+ {
+ var coil = new Coil
+ {
+ Address = address,
+ HighByte = requestBytes[10],
+ LowByte = requestBytes[11],
+ };
+
+ bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[6], coil, cancellationToken).ConfigureAwait(false);
+ if (isSuccess)
+ {
+ // Response is an echo of the request
+ responseBytes.AddRange(requestBytes.Skip(8).Take(4));
+ }
+ else
+ {
+ responseBytes[7] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
+ }
+ }
+ catch
+ {
+ responseBytes[7] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
+ }
+
+ return [.. responseBytes];
+ }
+
+ private async Task HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken)
+ {
+ if (requestBytes.Length < 12)
+ return null;
+
+ var responseBytes = new List();
+ responseBytes.AddRange(requestBytes.Take(8));
+
+ ushort address = requestBytes.GetBigEndianUInt16(8);
+
+ try
+ {
+ var register = new HoldingRegister
+ {
+ Address = address,
+ HighByte = requestBytes[10],
+ LowByte = requestBytes[11]
+ };
+
+ bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[6], register, cancellationToken).ConfigureAwait(false);
+ if (isSuccess)
+ {
+ // Response is an echo of the request
+ responseBytes.AddRange(requestBytes.Skip(8).Take(4));
+ }
+ else
+ {
+ responseBytes[7] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
+ }
+ }
+ catch
+ {
+ responseBytes[7] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
+ }
+
+ return [.. responseBytes];
+ }
+
+ private async Task HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
+ {
+ 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;
+ var coils = new List();
+ 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;
+
+ coils.Add(new Coil
+ {
+ Address = address,
+ HighByte = value ? (byte)0xFF : (byte)0x00
+ });
+ }
+
+ bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[6], coils, cancellationToken).ConfigureAwait(false);
+ if (isSuccess)
+ {
+ // Response is an echo of the request
+ responseBytes.AddRange(requestBytes.Skip(8).Take(4));
+ }
+ else
+ {
+ responseBytes[7] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
+ }
+ }
+ catch
+ {
+ responseBytes[7] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
+ }
+
+ return [.. responseBytes];
+ }
+
+ private async Task HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
+ {
+ 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;
+ var list = new List();
+ for (int i = 0; i < count; i++)
+ {
+ ushort address = (ushort)(firstAddress + i);
+
+ list.Add(new HoldingRegister
+ {
+ Address = address,
+ HighByte = requestBytes[baseOffset + i * 2],
+ LowByte = requestBytes[baseOffset + i * 2 + 1]
+ });
+
+ bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[6], list, cancellationToken).ConfigureAwait(false);
+ if (isSuccess)
+ {
+ // Response is an echo of the request
+ responseBytes.AddRange(requestBytes.Skip(8).Take(4));
+ }
+ else
+ {
+ responseBytes[7] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
+ }
+ }
+ }
+ catch
+ {
+ responseBytes[7] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
+ }
+
+ return [.. responseBytes];
+ }
+
+ private async Task HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken)
+ {
+ var responseBytes = new List();
+ responseBytes.AddRange(requestBytes.Take(8));
+
+ if (requestBytes[8] != 0x0E)
+ {
+ responseBytes[7] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
+ return [.. responseBytes];
+ }
+
+ var firstObject = (ModbusDeviceIdentificationObject)requestBytes[10];
+ 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 res = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken).ConfigureAwait(false);
+
+ var bodyBytes = new List();
+
+ // MEI, Category
+ bodyBytes.AddRange(requestBytes.Skip(8).Take(2));
+
+ // Conformity
+ bodyBytes.Add((byte)category);
+ if (res.IsIndividualAccessAllowed)
+ bodyBytes[2] |= 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
+ 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, res);
+
+ // 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, DeviceIdentification deviceIdentification)
+ {
+ var result = new List { objectId };
+ switch ((ModbusDeviceIdentificationObject)objectId)
+ {
+ case ModbusDeviceIdentificationObject.VendorName:
+ {
+ byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName);
+ result.Add((byte)bytes.Length);
+ result.AddRange(bytes);
+ }
+ break;
+
+ case ModbusDeviceIdentificationObject.ProductCode:
+ {
+ byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode);
+ result.Add((byte)bytes.Length);
+ result.AddRange(bytes);
+ }
+ break;
+
+ case ModbusDeviceIdentificationObject.MajorMinorRevision:
+ {
+ byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision);
+ result.Add((byte)bytes.Length);
+ result.AddRange(bytes);
+ }
+ break;
+
+ case ModbusDeviceIdentificationObject.VendorUrl:
+ {
+ byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl);
+ result.Add((byte)bytes.Length);
+ result.AddRange(bytes);
+ }
+ break;
+
+ case ModbusDeviceIdentificationObject.ProductName:
+ {
+ byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName);
+ result.Add((byte)bytes.Length);
+ result.AddRange(bytes);
+ }
+ break;
+
+ case ModbusDeviceIdentificationObject.ModelName:
+ {
+ byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName);
+ result.Add((byte)bytes.Length);
+ result.AddRange(bytes);
+ }
+ break;
+
+ case ModbusDeviceIdentificationObject.UserApplicationName:
+ {
+ byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName);
+ result.Add((byte)bytes.Length);
+ result.AddRange(bytes);
+ }
+ break;
+
+ default:
+ {
+ if (deviceIdentification.ExtendedObjects.ContainsKey(objectId))
+ {
+ byte[] bytes = deviceIdentification.ExtendedObjects[objectId];
+ result.Add((byte)bytes.Length);
+ result.AddRange(bytes);
+ }
+ else
+ {
+ result.Add(0x00);
+ }
+ }
+ break;
+ }
+
+ return [.. result];
+ }
+
+ #endregion Request Handling
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Proxy/README.md b/AMWD.Protocols.Modbus.Proxy/README.md
new file mode 100644
index 0000000..9fc877a
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Proxy/README.md
@@ -0,0 +1,10 @@
+# Modbus Protocol for .NET | Proxy
+
+With this package the server and client implementations will be combined as proxy.
+
+You can use any `ModbusBasClient` implementation as target client and plug it into the implemented `ModbusTcpProxy` or `ModbusRtuProxy`, which implement the server side.
+
+
+---
+
+Published under MIT License (see [**tl;dr**Legal](https://www.tldrlegal.com/license/mit-license))
diff --git a/AMWD.Protocols.Modbus.sln b/AMWD.Protocols.Modbus.sln
index 749066f..728d0d8 100644
--- a/AMWD.Protocols.Modbus.sln
+++ b/AMWD.Protocols.Modbus.sln
@@ -29,11 +29,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{C8065AE3
Directory.Build.props = Directory.Build.props
EndProjectSection
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMWD.Protocols.Modbus.Tests", "AMWD.Protocols.Modbus.Tests\AMWD.Protocols.Modbus.Tests.csproj", "{146070C4-E922-4F5A-AD6F-9A899186E26E}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Tests", "AMWD.Protocols.Modbus.Tests\AMWD.Protocols.Modbus.Tests.csproj", "{146070C4-E922-4F5A-AD6F-9A899186E26E}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMWD.Protocols.Modbus.Tcp", "AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj", "{8C888A84-CD09-4087-B5DA-67708ABBABA2}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Tcp", "AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj", "{8C888A84-CD09-4087-B5DA-67708ABBABA2}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMWD.Protocols.Modbus.Serial", "AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj", "{D966826F-EE6C-4BC0-9185-C2A9A50FD586}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Serial", "AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj", "{D966826F-EE6C-4BC0-9185-C2A9A50FD586}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMWD.Protocols.Modbus.Proxy", "AMWD.Protocols.Modbus.Proxy\AMWD.Protocols.Modbus.Proxy.csproj", "{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -57,6 +59,10 @@ Global
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f36987..7fbcc0a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- New `AMWD.Protocols.Modbus.Proxy` package, that contains the server implementations as proxies.
+
### Changed
- Renamed `ModbusSerialServer` to `ModbusRtuServer` so clearify the protocol, that is used.
diff --git a/README.md b/README.md
index e203a0d..2e151cd 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,11 @@ For example the default protocol versions: `TCP`, `RTU` and `ASCII`.
With this package you'll have anything you need to create your own client implementations.
+### [Proxy]
+
+The package contains a TCP and a RTU server implementation as proxy which contains a client of your choice to connect to.
+
+
### [Serial]
This package contains some wrappers and implementations for the serial protocol.
@@ -42,6 +47,7 @@ Published under [MIT License] (see [**tl;dr**Legal])
[see here]: https://github.com/andreasAMmueller/Modbus
[Common]: AMWD.Protocols.Modbus.Common/README.md
+[Proxy]: AMWD.Protocols.Modbus.Proxy/README.md
[Serial]: AMWD.Protocols.Modbus.Serial/README.md
[TCP]: AMWD.Protocols.Modbus.Tcp/README.md
[MIT License]: LICENSE.txt