From 54511c936604cfeb97cf03245d69b6714f97b29e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Sat, 20 Apr 2024 20:54:25 +0200 Subject: [PATCH] Added new Proxy package with Tcp implementation --- .../AMWD.Protocols.Modbus.Proxy.csproj | 30 + AMWD.Protocols.Modbus.Proxy/ModbusTcpProxy.cs | 853 ++++++++++++++++++ AMWD.Protocols.Modbus.Proxy/README.md | 10 + AMWD.Protocols.Modbus.sln | 12 +- CHANGELOG.md | 4 + README.md | 6 + 6 files changed, 912 insertions(+), 3 deletions(-) create mode 100644 AMWD.Protocols.Modbus.Proxy/AMWD.Protocols.Modbus.Proxy.csproj create mode 100644 AMWD.Protocols.Modbus.Proxy/ModbusTcpProxy.cs create mode 100644 AMWD.Protocols.Modbus.Proxy/README.md 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