8 Commits

24 changed files with 1980 additions and 83 deletions

View File

@@ -25,6 +25,8 @@ build-debug:
- mkdir ./artifacts - mkdir ./artifacts
- mv ./AMWD.Protocols.Modbus.Common/bin/Debug/*.nupkg ./artifacts/ - mv ./AMWD.Protocols.Modbus.Common/bin/Debug/*.nupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Common/bin/Debug/*.snupkg ./artifacts/ - mv ./AMWD.Protocols.Modbus.Common/bin/Debug/*.snupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Debug/*.nupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Debug/*.snupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Serial/bin/Debug/*.nupkg ./artifacts/ - mv ./AMWD.Protocols.Modbus.Serial/bin/Debug/*.nupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Serial/bin/Debug/*.snupkg ./artifacts/ - mv ./AMWD.Protocols.Modbus.Serial/bin/Debug/*.snupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Debug/*.nupkg ./artifacts/ - mv ./AMWD.Protocols.Modbus.Tcp/bin/Debug/*.nupkg ./artifacts/
@@ -80,6 +82,8 @@ build-release:
- mkdir ./artifacts - mkdir ./artifacts
- mv ./AMWD.Protocols.Modbus.Common/bin/Release/*.nupkg ./artifacts/ - mv ./AMWD.Protocols.Modbus.Common/bin/Release/*.nupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Common/bin/Release/*.snupkg ./artifacts/ - mv ./AMWD.Protocols.Modbus.Common/bin/Release/*.snupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Release/*.nupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Release/*.snupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Serial/bin/Release/*.nupkg ./artifacts/ - mv ./AMWD.Protocols.Modbus.Serial/bin/Release/*.nupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Serial/bin/Release/*.snupkg ./artifacts/ - mv ./AMWD.Protocols.Modbus.Serial/bin/Release/*.snupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Release/*.nupkg ./artifacts/ - mv ./AMWD.Protocols.Modbus.Tcp/bin/Release/*.nupkg ./artifacts/

View File

@@ -52,7 +52,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
/// <remarks> /// <remarks>
/// The default protocol used by the client should be initialized in the constructor. /// The default protocol used by the client should be initialized in the constructor.
/// </remarks> /// </remarks>
public abstract IModbusProtocol Protocol { get; set; } public virtual IModbusProtocol Protocol { get; set; }
/// <summary> /// <summary>
/// Reads multiple <see cref="Coil"/>s. /// Reads multiple <see cref="Coil"/>s.

View File

@@ -45,7 +45,7 @@ namespace System.Collections.Generic
internalDequeueTcs = ResetToken(ref _dequeueTcs); internalDequeueTcs = ResetToken(ref _dequeueTcs);
} }
await WaitAsync(internalDequeueTcs, cancellationToken).ConfigureAwait(false); await WaitAsync(internalDequeueTcs, cancellationToken);
} }
} }
@@ -113,7 +113,7 @@ namespace System.Collections.Generic
{ {
if (await Task.WhenAny(tcs.Task, Task.Delay(-1, cancellationToken)) == tcs.Task) if (await Task.WhenAny(tcs.Task, Task.Delay(-1, cancellationToken)) == tcs.Task)
{ {
await tcs.Task.ConfigureAwait(false); await tcs.Task;
return; return;
} }

View File

@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
<LangVersion>12.0</LangVersion>
<PackageId>AMWD.Protocols.Modbus.Proxy</PackageId>
<AssemblyName>amwd-modbus-proxy</AssemblyName>
<RootNamespace>AMWD.Protocols.Modbus.Proxy</RootNamespace>
<Product>Modbus Proxy Clients</Product>
<Description>Plugging Modbus Servers and Clients together to create Modbus Proxies.</Description>
<PackageTags>Modbus Protocol Proxy</PackageTags>
</PropertyGroup>
<ItemGroup>
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" />
<Compile Include="../AMWD.Protocols.Modbus.Tcp/Extensions/StreamExtensions.cs" Link="Extensions/StreamExtensions.cs" />
</ItemGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="/" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="System.IO.Ports" Version="4.7.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
<PackageReference Include="System.IO.Ports" Version="6.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
<ProjectReference Include="..\AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,867 @@
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
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;
using AMWD.Protocols.Modbus.Serial;
namespace AMWD.Protocols.Modbus.Proxy
{
/// <summary>
/// Implements a Modbus serial line RTU server proxying all requests to a Modbus client of choice.
/// </summary>
public class ModbusRtuProxy : IDisposable
{
#region Fields
private bool _isDisposed;
private readonly SerialPort _serialPort;
private CancellationTokenSource _stopCts;
#endregion Fields
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ModbusRtuProxy"/> class.
/// </summary>
/// <param name="client">The <see cref="ModbusClientBase"/> used to request the remote device, that should be proxied.</param>
/// <param name="portName">The name of the serial port to use.</param>
/// <param name="baudRate">The baud rate of the serial port (Default: 19.200).</param>
public ModbusRtuProxy(ModbusClientBase client, string portName, BaudRate baudRate = BaudRate.Baud19200)
{
Client = client ?? throw new ArgumentNullException(nameof(client));
if (string.IsNullOrWhiteSpace(portName))
throw new ArgumentNullException(nameof(portName));
if (!Enum.IsDefined(typeof(BaudRate), baudRate))
throw new ArgumentOutOfRangeException(nameof(baudRate));
if (!ModbusSerialClient.AvailablePortNames.Contains(portName))
throw new ArgumentException($"The serial port ({portName}) is not available.", nameof(portName));
_serialPort = new SerialPort
{
PortName = portName,
BaudRate = (int)baudRate,
Handshake = Handshake.None,
DataBits = 8,
ReadTimeout = 1000,
RtsEnable = false,
StopBits = StopBits.One,
WriteTimeout = 1000,
Parity = Parity.Even
};
}
#endregion Constructors
#region Properties
/// <summary>
/// Gets the Modbus client used to request the remote device, that should be proxied.
/// </summary>
public ModbusClientBase Client { get; }
/// <inheritdoc cref="SerialPort.PortName"/>
public string PortName => _serialPort.PortName;
/// <summary>
/// Gets or sets the baud rate of the serial port.
/// </summary>
public BaudRate BaudRate
{
get => (BaudRate)_serialPort.BaudRate;
set => _serialPort.BaudRate = (int)value;
}
/// <inheritdoc cref="SerialPort.Handshake"/>
public Handshake Handshake
{
get => _serialPort.Handshake;
set => _serialPort.Handshake = value;
}
/// <inheritdoc cref="SerialPort.DataBits"/>
public int DataBits
{
get => _serialPort.DataBits;
set => _serialPort.DataBits = value;
}
/// <inheritdoc cref="SerialPort.IsOpen"/>
public bool IsOpen => _serialPort.IsOpen;
/// <summary>
/// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a read operation does not finish.
/// </summary>
public TimeSpan ReadTimeout
{
get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout);
set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds;
}
/// <inheritdoc cref="SerialPort.RtsEnable"/>
public bool RtsEnable
{
get => _serialPort.RtsEnable;
set => _serialPort.RtsEnable = value;
}
/// <inheritdoc cref="SerialPort.StopBits"/>
public StopBits StopBits
{
get => _serialPort.StopBits;
set => _serialPort.StopBits = value;
}
/// <summary>
/// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a write operation does not finish.
/// </summary>
public TimeSpan WriteTimeout
{
get => TimeSpan.FromMilliseconds(_serialPort.WriteTimeout);
set => _serialPort.WriteTimeout = (int)value.TotalMilliseconds;
}
/// <inheritdoc cref="SerialPort.Parity"/>
public Parity Parity
{
get => _serialPort.Parity;
set => _serialPort.Parity = value;
}
#endregion Properties
#region Control Methods
/// <summary>
/// Starts the server.
/// </summary>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public Task StartAsync(CancellationToken cancellationToken = default)
{
Assertions();
_stopCts?.Cancel();
_serialPort.Close();
_serialPort.DataReceived -= OnDataReceived;
_stopCts?.Dispose();
_stopCts = new CancellationTokenSource();
_serialPort.DataReceived += OnDataReceived;
_serialPort.Open();
return Task.CompletedTask;
}
/// <summary>
/// Stops the server.
/// </summary>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public Task StopAsync(CancellationToken cancellationToken = default)
{
Assertions();
return StopAsyncInternal(cancellationToken);
}
private Task StopAsyncInternal(CancellationToken cancellationToken)
{
_stopCts.Cancel();
_serialPort.Close();
_serialPort.DataReceived -= OnDataReceived;
return Task.CompletedTask;
}
/// <summary>
/// Releases all managed and unmanaged resources used by the <see cref="ModbusRtuProxy"/>.
/// </summary>
public void Dispose()
{
if (_isDisposed)
return;
_isDisposed = true;
StopAsyncInternal(CancellationToken.None).Wait();
_serialPort.Dispose();
_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 void OnDataReceived(object _, SerialDataReceivedEventArgs evArgs)
{
try
{
var requestBytes = new List<byte>();
do
{
byte[] buffer = new byte[RtuProtocol.MAX_ADU_LENGTH];
int count = _serialPort.Read(buffer, 0, buffer.Length);
requestBytes.AddRange(buffer.Take(count));
_stopCts.Token.ThrowIfCancellationRequested();
}
while (_serialPort.BytesToRead > 0);
_stopCts.Token.ThrowIfCancellationRequested();
byte[] responseBytes = HandleRequest([.. requestBytes]);
if (responseBytes == null)
return;
_stopCts.Token.ThrowIfCancellationRequested();
_serialPort.Write(responseBytes, 0, responseBytes.Length);
}
catch
{ /* keep it quiet */ }
}
#endregion Client Handling
#region Request Handling
private byte[] HandleRequest(byte[] requestBytes)
{
byte[] recvCrc = requestBytes.Skip(requestBytes.Length - 2).ToArray();
byte[] calcCrc = RtuProtocol.CRC16(requestBytes, 0, requestBytes.Length - 2);
if (!recvCrc.SequenceEqual(calcCrc))
return null;
switch ((ModbusFunctionCode)requestBytes[1])
{
case ModbusFunctionCode.ReadCoils:
return HandleReadCoilsAsync(requestBytes, _stopCts.Token).Result;
case ModbusFunctionCode.ReadDiscreteInputs:
return HandleReadDiscreteInputsAsync(requestBytes, _stopCts.Token).Result;
case ModbusFunctionCode.ReadHoldingRegisters:
return HandleReadHoldingRegistersAsync(requestBytes, _stopCts.Token).Result;
case ModbusFunctionCode.ReadInputRegisters:
return HandleReadInputRegistersAsync(requestBytes, _stopCts.Token).Result;
case ModbusFunctionCode.WriteSingleCoil:
return HandleWriteSingleCoilAsync(requestBytes, _stopCts.Token).Result;
case ModbusFunctionCode.WriteSingleRegister:
return HandleWriteSingleRegisterAsync(requestBytes, _stopCts.Token).Result;
case ModbusFunctionCode.WriteMultipleCoils:
return HandleWriteMultipleCoilsAsync(requestBytes, _stopCts.Token).Result;
case ModbusFunctionCode.WriteMultipleRegisters:
return HandleWriteMultipleRegistersAsync(requestBytes, _stopCts.Token).Result;
case ModbusFunctionCode.EncapsulatedInterface:
return HandleEncapsulatedInterfaceAsync(requestBytes, _stopCts.Token).Result;
default: // unknown function
{
byte[] responseBytes = new byte[5];
Array.Copy(requestBytes, 0, responseBytes, 0, 2);
// Mark as error
responseBytes[1] |= 0x80;
responseBytes[2] = (byte)ModbusErrorCode.IllegalFunction;
SetCrc(responseBytes);
return responseBytes;
}
}
}
private async Task<byte[]> HandleReadCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
{
if (requestBytes.Length < 8)
return null;
byte unitId = requestBytes[0];
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
ushort count = requestBytes.GetBigEndianUInt16(4);
var responseBytes = new List<byte>();
responseBytes.AddRange(requestBytes.Take(2));
try
{
var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken);
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[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
AddCrc(responseBytes);
return [.. responseBytes];
}
private async Task<byte[]> HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken)
{
if (requestBytes.Length < 8)
return null;
byte unitId = requestBytes[0];
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
ushort count = requestBytes.GetBigEndianUInt16(4);
var responseBytes = new List<byte>();
responseBytes.AddRange(requestBytes.Take(2));
try
{
var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken);
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[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
AddCrc(responseBytes);
return [.. responseBytes];
}
private async Task<byte[]> HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
{
if (requestBytes.Length < 8)
return null;
byte unitId = requestBytes[0];
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
ushort count = requestBytes.GetBigEndianUInt16(4);
var responseBytes = new List<byte>();
responseBytes.AddRange(requestBytes.Take(2));
try
{
var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken);
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[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
AddCrc(responseBytes);
return [.. responseBytes];
}
private async Task<byte[]> HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
{
if (requestBytes.Length < 8)
return null;
byte unitId = requestBytes[0];
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
ushort count = requestBytes.GetBigEndianUInt16(4);
var responseBytes = new List<byte>();
responseBytes.AddRange(requestBytes.Take(2));
try
{
var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken);
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[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
AddCrc(responseBytes);
return [.. responseBytes];
}
private async Task<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken)
{
if (requestBytes.Length < 8)
return null;
var responseBytes = new List<byte>();
responseBytes.AddRange(requestBytes.Take(2));
ushort address = requestBytes.GetBigEndianUInt16(2);
if (requestBytes[4] != 0x00 && requestBytes[4] != 0xFF)
{
responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
AddCrc(responseBytes);
return [.. responseBytes];
}
try
{
var coil = new Coil
{
Address = address,
HighByte = requestBytes[4],
LowByte = requestBytes[5],
};
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[0], coil, cancellationToken);
if (isSuccess)
{
// Response is an echo of the request
responseBytes.AddRange(requestBytes.Skip(2).Take(4));
}
else
{
responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
}
catch
{
responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
AddCrc(responseBytes);
return [.. responseBytes];
}
private async Task<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken)
{
if (requestBytes.Length < 8)
return null;
var responseBytes = new List<byte>();
responseBytes.AddRange(requestBytes.Take(2));
ushort address = requestBytes.GetBigEndianUInt16(2);
try
{
var register = new HoldingRegister
{
Address = address,
HighByte = requestBytes[4],
LowByte = requestBytes[5]
};
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[0], register, cancellationToken);
if (isSuccess)
{
// Response is an echo of the request
responseBytes.AddRange(requestBytes.Skip(2).Take(4));
}
else
{
responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
}
catch
{
responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
AddCrc(responseBytes);
return [.. responseBytes];
}
private async Task<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
{
if (requestBytes.Length < 9)
return null;
var responseBytes = new List<byte>();
responseBytes.AddRange(requestBytes.Take(2));
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
ushort count = requestBytes.GetBigEndianUInt16(4);
int byteCount = (int)Math.Ceiling(count / 8.0);
if (requestBytes.Length < 9 + byteCount)
{
responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
AddCrc(responseBytes);
return [.. responseBytes];
}
try
{
int baseOffset = 7;
var coils = new List<Coil>();
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[0], coils, cancellationToken);
if (isSuccess)
{
// Response is an echo of the request
responseBytes.AddRange(requestBytes.Skip(2).Take(4));
}
else
{
responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
}
catch
{
responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
AddCrc(responseBytes);
return [.. responseBytes];
}
private async Task<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
{
if (requestBytes.Length < 9)
return null;
var responseBytes = new List<byte>();
responseBytes.AddRange(requestBytes.Take(8));
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
ushort count = requestBytes.GetBigEndianUInt16(4);
int byteCount = count * 2;
if (requestBytes.Length < 9 + byteCount)
{
responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
AddCrc(responseBytes);
return [.. responseBytes];
}
try
{
int baseOffset = 7;
var list = new List<HoldingRegister>();
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[0], list, cancellationToken);
if (isSuccess)
{
// Response is an echo of the request
responseBytes.AddRange(requestBytes.Skip(2).Take(4));
}
else
{
responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
}
}
catch
{
responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
AddCrc(responseBytes);
return [.. responseBytes];
}
private async Task<byte[]> HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken)
{
var responseBytes = new List<byte>();
responseBytes.AddRange(requestBytes.Take(2));
if (requestBytes[2] != 0x0E)
{
responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
AddCrc(responseBytes);
return [.. responseBytes];
}
var firstObject = (ModbusDeviceIdentificationObject)requestBytes[4];
if (0x06 < requestBytes[4] && requestBytes[4] < 0x80)
{
responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
AddCrc(responseBytes);
return [.. responseBytes];
}
var category = (ModbusDeviceIdentificationCategory)requestBytes[3];
if (!Enum.IsDefined(typeof(ModbusDeviceIdentificationCategory), category))
{
responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
AddCrc(responseBytes);
return [.. responseBytes];
}
try
{
var res = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken);
var bodyBytes = new List<byte>();
// MEI, Category
bodyBytes.AddRange(requestBytes.Skip(2).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[4];
break;
}
byte numberOfObjects = 0;
for (int i = requestBytes[4]; 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 > RtuProtocol.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);
AddCrc(responseBytes);
return [.. responseBytes];
}
catch
{
responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
AddCrc(responseBytes);
return [.. responseBytes];
}
}
private byte[] GetDeviceObject(byte objectId, DeviceIdentification deviceIdentification)
{
var result = new List<byte> { 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];
}
private static void SetCrc(byte[] bytes)
{
byte[] crc = RtuProtocol.CRC16(bytes, 0, bytes.Length - 2);
bytes[bytes.Length - 2] = crc[0];
bytes[bytes.Length - 1] = crc[1];
}
private static void AddCrc(List<byte> bytes)
{
byte[] crc = RtuProtocol.CRC16(bytes);
bytes.Add(crc[0]);
bytes.Add(crc[1]);
}
#endregion Request Handling
}
}

View File

@@ -0,0 +1,855 @@
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
{
/// <summary>
/// Implements a Modbus TCP server proxying all requests to a Modbus client of choice.
/// </summary>
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<TcpClient> _clients = [];
private readonly List<Task> _clientTasks = [];
#endregion Fields
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ModbusTcpProxy"/> class.
/// </summary>
/// <param name="client">The <see cref="ModbusClientBase"/> used to request the remote device, that should be proxied.</param>
/// <param name="listenAddress">An <see cref="IPAddress"/> to listen on (Default: <see cref="IPAddress.Loopback"/>).</param>
/// <param name="listenPort">A port to listen on (Default: 502).</param>
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
/// <summary>
/// Gets the Modbus client used to request the remote device, that should be proxied.
/// </summary>
public ModbusClientBase Client { get; }
/// <summary>
/// Gets the <see cref="IPAddress"/> to listen on.
/// </summary>
public IPAddress ListenAddress { get; }
/// <summary>
/// Get the port to listen on.
/// </summary>
public int ListenPort { get; }
/// <summary>
/// Gets a value indicating whether the server is running.
/// </summary>
public bool IsRunning => _listener?.Server.IsBound ?? false;
/// <summary>
/// Gets or sets the read/write timeout for the incoming connections (not the <see cref="Client"/>!).
/// </summary>
public TimeSpan ReadWriteTimeout { get; set; }
#endregion Properties
#region Control Methods
/// <summary>
/// Starts the server.
/// </summary>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
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;
}
/// <summary>
/// Stops the server.
/// </summary>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
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
}
}
/// <summary>
/// Releases all managed and unmanaged resources used by the <see cref="ModbusTcpProxy"/>.
/// </summary>
public void Dispose()
{
if (_isDisposed)
return;
_isDisposed = true;
StopAsyncInternal(CancellationToken.None).Wait();
_clientListLock.Dispose();
_clients.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<byte>();
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 = await HandleRequestAsync([.. requestBytes], cancellationToken);
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 Task<byte[]> 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<byte[]> 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<byte>();
responseBytes.AddRange(requestBytes.Take(8));
try
{
var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken);
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<byte[]> 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<byte>();
responseBytes.AddRange(requestBytes.Take(8));
try
{
var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken);
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<byte[]> 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<byte>();
responseBytes.AddRange(requestBytes.Take(8));
try
{
var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken);
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<byte[]> 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<byte>();
responseBytes.AddRange(requestBytes.Take(8));
try
{
var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken);
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<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken)
{
if (requestBytes.Length < 12)
return null;
var responseBytes = new List<byte>();
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);
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<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken)
{
if (requestBytes.Length < 12)
return null;
var responseBytes = new List<byte>();
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);
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<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
{
if (requestBytes.Length < 13)
return null;
var responseBytes = new List<byte>();
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<Coil>();
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);
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<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
{
if (requestBytes.Length < 13)
return null;
var responseBytes = new List<byte>();
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<HoldingRegister>();
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);
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<byte[]> HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken)
{
var responseBytes = new List<byte>();
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);
var bodyBytes = new List<byte>();
// 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<byte> { 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
}
}

View File

@@ -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))

View File

@@ -14,10 +14,10 @@ using AMWD.Protocols.Modbus.Common.Protocols;
namespace AMWD.Protocols.Modbus.Serial namespace AMWD.Protocols.Modbus.Serial
{ {
/// <summary> /// <summary>
/// A basic implementation of a Modbus serial line server. /// A basic implementation of a Modbus serial line RTU server.
/// </summary> /// </summary>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public class ModbusSerialServer : IDisposable public class ModbusRtuServer : IDisposable
{ {
#region Fields #region Fields
@@ -34,11 +34,11 @@ namespace AMWD.Protocols.Modbus.Serial
#region Constructors #region Constructors
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ModbusSerialServer"/> class. /// Initializes a new instance of the <see cref="ModbusRtuServer"/> class.
/// </summary> /// </summary>
/// <param name="portName">The name of the serial port to use.</param> /// <param name="portName">The name of the serial port to use.</param>
/// <param name="baudRate">The baud rate of the serial port (Default: 19.200).</param> /// <param name="baudRate">The baud rate of the serial port (Default: 19.200).</param>
public ModbusSerialServer(string portName, BaudRate baudRate = BaudRate.Baud19200) public ModbusRtuServer(string portName, BaudRate baudRate = BaudRate.Baud19200)
{ {
if (string.IsNullOrWhiteSpace(portName)) if (string.IsNullOrWhiteSpace(portName))
throw new ArgumentNullException(nameof(portName)); throw new ArgumentNullException(nameof(portName));
@@ -195,7 +195,7 @@ namespace AMWD.Protocols.Modbus.Serial
} }
/// <summary> /// <summary>
/// Releases all managed and unmanaged resources used by the <see cref="ModbusSerialServer"/>. /// Releases all managed and unmanaged resources used by the <see cref="ModbusRtuServer"/>.
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
@@ -208,6 +208,9 @@ namespace AMWD.Protocols.Modbus.Serial
_deviceListLock.Dispose(); _deviceListLock.Dispose();
_devices.Clear(); _devices.Clear();
_serialPort.Dispose();
_stopCts?.Dispose();
} }
private void Assertions() private void Assertions()

View File

@@ -43,9 +43,6 @@ namespace AMWD.Protocols.Modbus.Serial
/// <inheritdoc cref="SerialPort.GetPortNames" /> /// <inheritdoc cref="SerialPort.GetPortNames" />
public static string[] AvailablePortNames => SerialPort.GetPortNames(); public static string[] AvailablePortNames => SerialPort.GetPortNames();
/// <inheritdoc/>
public override IModbusProtocol Protocol { get; set; }
/// <inheritdoc cref="IModbusConnection.IdleTimeout"/> /// <inheritdoc cref="IModbusConnection.IdleTimeout"/>
public TimeSpan IdleTimeout public TimeSpan IdleTimeout
{ {

View File

@@ -259,7 +259,7 @@ namespace AMWD.Protocols.Modbus.Serial
try try
{ {
// Get next request to process // Get next request to process
var item = await _requestQueue.DequeueAsync(cancellationToken).ConfigureAwait(false); var item = await _requestQueue.DequeueAsync(cancellationToken);
// Remove registration => already removed from queue // Remove registration => already removed from queue
item.CancellationTokenRegistration.Dispose(); item.CancellationTokenRegistration.Dispose();
@@ -267,13 +267,13 @@ namespace AMWD.Protocols.Modbus.Serial
// Build combined cancellation token // Build combined cancellation token
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
// Wait for exclusive access // Wait for exclusive access
await _portLock.WaitAsync(linkedCts.Token).ConfigureAwait(false); await _portLock.WaitAsync(linkedCts.Token);
try try
{ {
// Ensure connection is up // Ensure connection is up
await AssertConnection(linkedCts.Token).ConfigureAwait(false); await AssertConnection(linkedCts.Token);
await _serialPort.WriteAsync(item.Request, linkedCts.Token).ConfigureAwait(false); await _serialPort.WriteAsync(item.Request, linkedCts.Token);
linkedCts.Token.ThrowIfCancellationRequested(); linkedCts.Token.ThrowIfCancellationRequested();
@@ -282,7 +282,7 @@ namespace AMWD.Protocols.Modbus.Serial
do do
{ {
int readCount = await _serialPort.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token).ConfigureAwait(false); int readCount = await _serialPort.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token);
if (readCount < 1) if (readCount < 1)
throw new EndOfStreamException(); throw new EndOfStreamException();
@@ -313,7 +313,7 @@ namespace AMWD.Protocols.Modbus.Serial
_portLock.Release(); _portLock.Release();
_idleTimer.Change(IdleTimeout, Timeout.InfiniteTimeSpan); _idleTimer.Change(IdleTimeout, Timeout.InfiniteTimeSpan);
await Task.Delay(InterRequestDelay, cancellationToken).ConfigureAwait(false); await Task.Delay(InterRequestDelay, cancellationToken);
} }
} }
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
@@ -370,7 +370,7 @@ namespace AMWD.Protocols.Modbus.Serial
try try
{ {
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(false); await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken);
} }
catch catch
{ /* keep it quiet */ } { /* keep it quiet */ }

View File

@@ -22,6 +22,19 @@ float voltage = registers.GetSingle();
Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V"); Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V");
``` ```
If you want to use the `ASCII` protocol instead, you can do this on initialization:
```csharp
// [...]
using var client = new ModbusSerialClient(serialPort)
{
Protocol = new AsciiProtocol();
};
// [...]
```
## Sources ## Sources

View File

@@ -145,7 +145,7 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
try try
{ {
return await _serialPort.BaseStream.ReadAsync(buffer, offset, count, cts.Token).ConfigureAwait(false); return await _serialPort.BaseStream.ReadAsync(buffer, offset, count, cts.Token);
} }
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{ {
@@ -195,9 +195,9 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
try try
{ {
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
await _serialPort.BaseStream.WriteAsync(buffer, cts.Token).ConfigureAwait(false); await _serialPort.BaseStream.WriteAsync(buffer, cts.Token);
#else #else
await _serialPort.BaseStream.WriteAsync(buffer, 0, buffer.Length, cts.Token).ConfigureAwait(false); await _serialPort.BaseStream.WriteAsync(buffer, 0, buffer.Length, cts.Token);
#endif #endif
} }
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)

View File

@@ -11,7 +11,7 @@ namespace System.IO
int offset = 0; int offset = 0;
do do
{ {
int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken).ConfigureAwait(false); int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken);
if (count < 1) if (count < 1)
throw new EndOfStreamException(); throw new EndOfStreamException();

View File

@@ -40,9 +40,6 @@ namespace AMWD.Protocols.Modbus.Tcp
Protocol = new TcpProtocol(); Protocol = new TcpProtocol();
} }
/// <inheritdoc/>
public override IModbusProtocol Protocol { get; set; }
/// <inheritdoc cref="IModbusConnection.IdleTimeout"/> /// <inheritdoc cref="IModbusConnection.IdleTimeout"/>
public TimeSpan IdleTimeout public TimeSpan IdleTimeout
{ {

View File

@@ -26,13 +26,17 @@ namespace AMWD.Protocols.Modbus.Tcp
private bool _isDisposed; private bool _isDisposed;
private readonly CancellationTokenSource _disposeCts = new(); private readonly CancellationTokenSource _disposeCts = new();
private readonly TcpClientWrapperFactory _tcpClientFactory = new();
private readonly SemaphoreSlim _clientLock = new(1, 1); private readonly SemaphoreSlim _clientLock = new(1, 1);
private readonly TcpClientWrapper _tcpClient = new(); private TcpClientWrapper _tcpClient = null;
private readonly Timer _idleTimer; private readonly Timer _idleTimer;
private readonly Task _processingTask; private readonly Task _processingTask;
private readonly AsyncQueue<RequestQueueItem> _requestQueue = new(); private readonly AsyncQueue<RequestQueueItem> _requestQueue = new();
private TimeSpan _readTimeout = TimeSpan.FromMilliseconds(1);
private TimeSpan _writeTimeout = TimeSpan.FromMilliseconds(1);
#endregion Fields #endregion Fields
/// <summary> /// <summary>
@@ -58,15 +62,33 @@ namespace AMWD.Protocols.Modbus.Tcp
/// <inheritdoc/> /// <inheritdoc/>
public virtual TimeSpan ReadTimeout public virtual TimeSpan ReadTimeout
{ {
get => TimeSpan.FromMilliseconds(_tcpClient.ReceiveTimeout); get => _readTimeout;
set => _tcpClient.ReceiveTimeout = (int)value.TotalMilliseconds; set
{
if (value < TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(value));
_readTimeout = value;
if (_tcpClient != null)
_tcpClient.ReceiveTimeout = (int)value.TotalMilliseconds;
}
} }
/// <inheritdoc/> /// <inheritdoc/>
public virtual TimeSpan WriteTimeout public virtual TimeSpan WriteTimeout
{ {
get => TimeSpan.FromMilliseconds(_tcpClient.SendTimeout); get => _writeTimeout;
set => _tcpClient.SendTimeout = (int)value.TotalMilliseconds; set
{
if (value < TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(value));
_writeTimeout = value;
if (_tcpClient != null)
_tcpClient.SendTimeout = (int)value.TotalMilliseconds;
}
} }
/// <summary> /// <summary>
@@ -116,7 +138,6 @@ namespace AMWD.Protocols.Modbus.Tcp
try try
{ {
_processingTask.Wait();
_processingTask.Dispose(); _processingTask.Dispose();
} }
catch catch
@@ -124,7 +145,7 @@ namespace AMWD.Protocols.Modbus.Tcp
OnIdleTimer(null); OnIdleTimer(null);
_tcpClient.Dispose(); _tcpClient?.Dispose();
_clientLock.Dispose(); _clientLock.Dispose();
while (_requestQueue.TryDequeue(out var item)) while (_requestQueue.TryDequeue(out var item))
@@ -164,7 +185,7 @@ namespace AMWD.Protocols.Modbus.Tcp
{ {
Request = [.. request], Request = [.. request],
ValidateResponseComplete = validateResponseComplete, ValidateResponseComplete = validateResponseComplete,
TaskCompletionSource = new(), TaskCompletionSource = new TaskCompletionSource<IReadOnlyList<byte>>(),
CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)
}; };
@@ -187,7 +208,7 @@ namespace AMWD.Protocols.Modbus.Tcp
try try
{ {
// Get next request to process // Get next request to process
var item = await _requestQueue.DequeueAsync(cancellationToken).ConfigureAwait(false); var item = await _requestQueue.DequeueAsync(cancellationToken);
// Remove registration => already removed from queue // Remove registration => already removed from queue
item.CancellationTokenRegistration.Dispose(); item.CancellationTokenRegistration.Dispose();
@@ -195,19 +216,19 @@ namespace AMWD.Protocols.Modbus.Tcp
// Build combined cancellation token // Build combined cancellation token
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
// Wait for exclusive access // Wait for exclusive access
await _clientLock.WaitAsync(linkedCts.Token).ConfigureAwait(false); await _clientLock.WaitAsync(linkedCts.Token);
try try
{ {
// Ensure connection is up // Ensure connection is up
await AssertConnection(linkedCts.Token).ConfigureAwait(false); await AssertConnection(linkedCts.Token);
var stream = _tcpClient.GetStream(); var stream = _tcpClient.GetStream();
await stream.FlushAsync(linkedCts.Token).ConfigureAwait(false); await stream.FlushAsync(linkedCts.Token);
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
await stream.WriteAsync(item.Request, linkedCts.Token).ConfigureAwait(false); await stream.WriteAsync(item.Request, linkedCts.Token);
#else #else
await stream.WriteAsync(item.Request, 0, item.Request.Length, linkedCts.Token).ConfigureAwait(false); await stream.WriteAsync(item.Request, 0, item.Request.Length, linkedCts.Token);
#endif #endif
linkedCts.Token.ThrowIfCancellationRequested(); linkedCts.Token.ThrowIfCancellationRequested();
@@ -218,9 +239,9 @@ namespace AMWD.Protocols.Modbus.Tcp
do do
{ {
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
int readCount = await stream.ReadAsync(buffer, linkedCts.Token).ConfigureAwait(false); int readCount = await stream.ReadAsync(buffer, linkedCts.Token);
#else #else
int readCount = await stream.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token).ConfigureAwait(false); int readCount = await stream.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token);
#endif #endif
if (readCount < 1) if (readCount < 1)
throw new EndOfStreamException(); throw new EndOfStreamException();
@@ -267,7 +288,7 @@ namespace AMWD.Protocols.Modbus.Tcp
// Has to be called within _clientLock! // Has to be called within _clientLock!
private async Task AssertConnection(CancellationToken cancellationToken) private async Task AssertConnection(CancellationToken cancellationToken)
{ {
if (_tcpClient.Connected) if (_tcpClient?.Connected == true)
return; return;
int delay = 1; int delay = 1;
@@ -284,14 +305,16 @@ namespace AMWD.Protocols.Modbus.Tcp
{ {
foreach (var ipAddress in ipAddresses) foreach (var ipAddress in ipAddresses)
{ {
_tcpClient.Close(); _tcpClient?.Close();
_tcpClient?.Dispose();
_tcpClient = _tcpClientFactory.Create(ipAddress.AddressFamily, _readTimeout, _writeTimeout);
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
using var connectTask = _tcpClient.ConnectAsync(ipAddress, Port, cancellationToken); var connectTask = _tcpClient.ConnectAsync(ipAddress, Port, cancellationToken);
#else #else
using var connectTask = _tcpClient.ConnectAsync(ipAddress, Port); var connectTask = _tcpClient.ConnectAsync(ipAddress, Port);
#endif #endif
if (await Task.WhenAny(connectTask, Task.Delay(ReadTimeout, cancellationToken)) == connectTask) if (await Task.WhenAny(connectTask, Task.Delay(_readTimeout, cancellationToken)) == connectTask)
{ {
await connectTask; await connectTask;
if (_tcpClient.Connected) if (_tcpClient.Connected)
@@ -309,7 +332,7 @@ namespace AMWD.Protocols.Modbus.Tcp
try try
{ {
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(false); await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken);
} }
catch catch
{ /* keep it quiet */ } { /* keep it quiet */ }

View File

@@ -193,6 +193,8 @@ namespace AMWD.Protocols.Modbus.Tcp
_clients.Clear(); _clients.Clear();
_devices.Clear(); _devices.Clear();
_stopCts?.Dispose();
} }
private void Assertions() private void Assertions()
@@ -216,11 +218,11 @@ namespace AMWD.Protocols.Modbus.Tcp
try try
{ {
#if NET8_0_OR_GREATER #if NET8_0_OR_GREATER
var client = await _listener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(false); var client = await _listener.AcceptTcpClientAsync(cancellationToken);
#else #else
var client = await _listener.AcceptTcpClientAsync().ConfigureAwait(false); var client = await _listener.AcceptTcpClientAsync();
#endif #endif
await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(false); await _clientListLock.WaitAsync(cancellationToken);
try try
{ {
_clients.Add(client); _clients.Add(client);
@@ -250,20 +252,20 @@ namespace AMWD.Protocols.Modbus.Tcp
using (var cts = new CancellationTokenSource(ReadWriteTimeout)) using (var cts = new CancellationTokenSource(ReadWriteTimeout))
using (cancellationToken.Register(cts.Cancel)) using (cancellationToken.Register(cts.Cancel))
{ {
byte[] headerBytes = await stream.ReadExpectedBytesAsync(6, cts.Token).ConfigureAwait(false); byte[] headerBytes = await stream.ReadExpectedBytesAsync(6, cts.Token);
requestBytes.AddRange(headerBytes); requestBytes.AddRange(headerBytes);
byte[] followingCountBytes = headerBytes.Skip(4).Take(2).ToArray(); byte[] followingCountBytes = headerBytes.Skip(4).Take(2).ToArray();
followingCountBytes.SwapBigEndian(); followingCountBytes.SwapBigEndian();
int followingCount = BitConverter.ToUInt16(followingCountBytes, 0); int followingCount = BitConverter.ToUInt16(followingCountBytes, 0);
byte[] bodyBytes = await stream.ReadExpectedBytesAsync(followingCount, cts.Token).ConfigureAwait(false); byte[] bodyBytes = await stream.ReadExpectedBytesAsync(followingCount, cts.Token);
requestBytes.AddRange(bodyBytes); requestBytes.AddRange(bodyBytes);
} }
byte[] responseBytes = HandleRequest([.. requestBytes]); byte[] responseBytes = HandleRequest([.. requestBytes]);
if (responseBytes != null) if (responseBytes != null)
await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken).ConfigureAwait(false); await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken);
} }
} }
catch catch
@@ -272,7 +274,7 @@ namespace AMWD.Protocols.Modbus.Tcp
} }
finally finally
{ {
await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(false); await _clientListLock.WaitAsync(cancellationToken);
try try
{ {
_clients.Remove(client); _clients.Remove(client);

View File

@@ -23,6 +23,18 @@ float voltage = registers.GetSingle();
Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V"); Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V");
``` ```
If you want to use the `RTU over TCP` protocol instead, you can do this on initialization:
```csharp
// [...]
using var client = new ModbusTcpClient(host, port)
{
Protocol = new RtuOverTcpProtocol();
};
// [...]
```
## Sources ## Sources

View File

@@ -8,11 +8,11 @@ namespace AMWD.Protocols.Modbus.Tcp.Utils
{ {
/// <inheritdoc cref="TcpClient" /> /// <inheritdoc cref="TcpClient" />
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal class TcpClientWrapper : IDisposable internal class TcpClientWrapper(AddressFamily addressFamily) : IDisposable
{ {
#region Fields #region Fields
private readonly TcpClient _client = new(); private readonly TcpClient _client = new(addressFamily);
#endregion Fields #endregion Fields

View File

@@ -0,0 +1,26 @@
using System;
using System.Net.Sockets;
namespace AMWD.Protocols.Modbus.Tcp.Utils
{
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal class TcpClientWrapperFactory
{
/// <summary>
/// Creates a new instance of <see cref="TcpClientWrapper"/>.
/// </summary>
/// <param name="addressFamily">The <see cref="AddressFamily"/> of the <see cref="TcpClient"/> to use.</param>
/// <param name="readTimeout">The read timeout.</param>
/// <param name="writeTimeout">The write timeout.</param>
/// <returns>A new <see cref="TcpClientWrapper"/> instance.</returns>
public virtual TcpClientWrapper Create(AddressFamily addressFamily, TimeSpan readTimeout, TimeSpan writeTimeout)
{
var client = new TcpClientWrapper(addressFamily)
{
ReceiveTimeout = (int)readTimeout.TotalMilliseconds,
SendTimeout = (int)writeTimeout.TotalMilliseconds
};
return client;
}
}
}

View File

@@ -13,7 +13,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="coverlet.msbuild" Version="6.0.1"> <PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Net.Sockets;
using System.Reflection; using System.Reflection;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -16,6 +17,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
private readonly string _hostname = "127.0.0.1"; private readonly string _hostname = "127.0.0.1";
private Mock<TcpClientWrapper> _tcpClientMock; private Mock<TcpClientWrapper> _tcpClientMock;
private Mock<TcpClientWrapperFactory> _tcpClientFactoryMock;
private Mock<NetworkStreamWrapper> _networkStreamMock; private Mock<NetworkStreamWrapper> _networkStreamMock;
private bool _alwaysConnected; private bool _alwaysConnected;
@@ -40,10 +42,19 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
} }
[TestMethod] [TestMethod]
public void ShouldGetAndSetPropertiesOfBaseClient() public async Task ShouldSetPropertiesOfBaseClient()
{ {
// Arrange // Arrange
byte[] request = [1, 2, 3];
byte[] expectedResponse = [9, 8, 7];
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
_networkResponseQueue.Enqueue(expectedResponse);
var connection = GetTcpConnection(); var connection = GetTcpConnection();
await connection.InvokeAsync(request, validation);
_tcpClientMock.Invocations.Clear();
_networkStreamMock.Invocations.Clear();
// Act // Act
connection.ReadTimeout = TimeSpan.FromSeconds(123); connection.ReadTimeout = TimeSpan.FromSeconds(123);
@@ -51,8 +62,8 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
// Assert - part 1 // Assert - part 1
Assert.AreEqual("TCP", connection.Name); Assert.AreEqual("TCP", connection.Name);
Assert.AreEqual(1, connection.ReadTimeout.TotalSeconds); Assert.AreEqual(123, connection.ReadTimeout.TotalSeconds);
Assert.AreEqual(1, connection.WriteTimeout.TotalSeconds); Assert.AreEqual(456, connection.WriteTimeout.TotalSeconds);
Assert.AreEqual(_hostname, connection.Hostname); Assert.AreEqual(_hostname, connection.Hostname);
Assert.AreEqual(502, connection.Port); Assert.AreEqual(502, connection.Port);
@@ -61,9 +72,6 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
_tcpClientMock.VerifySet(c => c.ReceiveTimeout = 123000, Times.Once); _tcpClientMock.VerifySet(c => c.ReceiveTimeout = 123000, Times.Once);
_tcpClientMock.VerifySet(c => c.SendTimeout = 456000, Times.Once); _tcpClientMock.VerifySet(c => c.SendTimeout = 456000, Times.Once);
_tcpClientMock.VerifyGet(c => c.ReceiveTimeout, Times.Once);
_tcpClientMock.VerifyGet(c => c.SendTimeout, Times.Once);
_tcpClientMock.VerifyNoOtherCalls(); _tcpClientMock.VerifyNoOtherCalls();
_networkStreamMock.VerifyNoOtherCalls(); _networkStreamMock.VerifyNoOtherCalls();
} }
@@ -173,6 +181,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
CollectionAssert.AreEqual(expectedResponse, response.ToArray()); CollectionAssert.AreEqual(expectedResponse, response.ToArray());
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First()); CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
_tcpClientMock.Verify(c => c.Connected, Times.Once); _tcpClientMock.Verify(c => c.Connected, Times.Once);
_tcpClientMock.Verify(c => c.GetStream(), Times.Once); _tcpClientMock.Verify(c => c.GetStream(), Times.Once);
@@ -211,11 +220,10 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
CollectionAssert.AreEqual(expectedResponse, response.ToArray()); CollectionAssert.AreEqual(expectedResponse, response.ToArray());
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First()); CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
_tcpClientMock.VerifyGet(c => c.ReceiveTimeout, Times.Once);
_tcpClientMock.Verify(c => c.Connected, Times.Exactly(3)); _tcpClientMock.Verify(c => c.Connected, Times.Exactly(3));
_tcpClientMock.Verify(c => c.Close(), Times.Exactly(2)); _tcpClientMock.Verify(c => c.Close(), Times.Exactly(2));
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once); _tcpClientMock.Verify(c => c.Dispose(), Times.Once);
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
_tcpClientMock.Verify(c => c.GetStream(), Times.Once); _tcpClientMock.Verify(c => c.GetStream(), Times.Once);
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once); _networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
@@ -289,11 +297,10 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
CollectionAssert.AreEqual(expectedResponse, response.ToArray()); CollectionAssert.AreEqual(expectedResponse, response.ToArray());
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First()); CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
_tcpClientMock.VerifyGet(c => c.ReceiveTimeout, Times.Once);
_tcpClientMock.Verify(c => c.Connected, Times.Exactly(3)); _tcpClientMock.Verify(c => c.Connected, Times.Exactly(3));
_tcpClientMock.Verify(c => c.Close(), Times.Once); _tcpClientMock.Verify(c => c.Close(), Times.Once);
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once); _tcpClientMock.Verify(c => c.Dispose(), Times.Once);
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
_tcpClientMock.Verify(c => c.GetStream(), Times.Once); _tcpClientMock.Verify(c => c.GetStream(), Times.Once);
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once); _networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
@@ -329,11 +336,10 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
CollectionAssert.AreEqual(expectedResponse, response.ToArray()); CollectionAssert.AreEqual(expectedResponse, response.ToArray());
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First()); CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
_tcpClientMock.VerifyGet(c => c.ReceiveTimeout, Times.Exactly(2));
_tcpClientMock.Verify(c => c.Connected, Times.Exactly(3)); _tcpClientMock.Verify(c => c.Connected, Times.Exactly(3));
_tcpClientMock.Verify(c => c.Close(), Times.Exactly(2)); _tcpClientMock.Verify(c => c.Close(), Times.Exactly(2));
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Exactly(2)); _tcpClientMock.Verify(c => c.Dispose(), Times.Exactly(2));
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Exactly(3));
_tcpClientMock.Verify(c => c.GetStream(), Times.Once); _tcpClientMock.Verify(c => c.GetStream(), Times.Once);
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once); _networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
@@ -426,6 +432,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
CollectionAssert.AreEqual(expectedResponse, response.ToArray()); CollectionAssert.AreEqual(expectedResponse, response.ToArray());
_tcpClientMock.Verify(c => c.Connected, Times.Once); _tcpClientMock.Verify(c => c.Connected, Times.Once);
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
_tcpClientMock.Verify(c => c.GetStream(), Times.Once); _tcpClientMock.Verify(c => c.GetStream(), Times.Once);
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once); _networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
@@ -475,6 +482,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First()); CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
_tcpClientMock.Verify(c => c.Connected, Times.Once); _tcpClientMock.Verify(c => c.Connected, Times.Once);
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
_tcpClientMock.Verify(c => c.GetStream(), Times.Once); _tcpClientMock.Verify(c => c.GetStream(), Times.Once);
_tcpClientMock.Verify(c => c.Dispose(), Times.Once); _tcpClientMock.Verify(c => c.Dispose(), Times.Once);
@@ -508,7 +516,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
return ValueTask.FromResult(0); return ValueTask.FromResult(0);
}); });
_tcpClientMock = new Mock<TcpClientWrapper>(); _tcpClientMock = new Mock<TcpClientWrapper>(AddressFamily.Unknown);
_tcpClientMock.Setup(c => c.Connected).Returns(() => _alwaysConnected || _connectedQueue.Dequeue()); _tcpClientMock.Setup(c => c.Connected).Returns(() => _alwaysConnected || _connectedQueue.Dequeue());
_tcpClientMock.Setup(c => c.ReceiveTimeout).Returns(() => _clientReceiveTimeout); _tcpClientMock.Setup(c => c.ReceiveTimeout).Returns(() => _clientReceiveTimeout);
_tcpClientMock.Setup(c => c.SendTimeout).Returns(() => _clientSendTimeout); _tcpClientMock.Setup(c => c.SendTimeout).Returns(() => _clientSendTimeout);
@@ -521,6 +529,11 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
.Setup(c => c.GetStream()) .Setup(c => c.GetStream())
.Returns(() => _networkStreamMock.Object); .Returns(() => _networkStreamMock.Object);
_tcpClientFactoryMock = new Mock<TcpClientWrapperFactory>();
_tcpClientFactoryMock
.Setup(c => c.Create(It.IsAny<AddressFamily>(), It.IsAny<TimeSpan>(), It.IsAny<TimeSpan>()))
.Returns(_tcpClientMock.Object);
var connection = new ModbusTcpConnection var connection = new ModbusTcpConnection
{ {
Hostname = _hostname, Hostname = _hostname,
@@ -528,9 +541,9 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
}; };
// Replace real connection with mock // Replace real connection with mock
var connectionField = connection.GetType().GetField("_tcpClient", BindingFlags.NonPublic | BindingFlags.Instance); var factoryField = connection.GetType().GetField("_tcpClientFactory", BindingFlags.NonPublic | BindingFlags.Instance);
(connectionField.GetValue(connection) as TcpClientWrapper)?.Dispose(); (factoryField.GetValue(connection) as TcpClientWrapper)?.Dispose();
connectionField.SetValue(connection, _tcpClientMock.Object); factoryField.SetValue(connection, _tcpClientFactoryMock.Object);
return connection; return connection;
} }

View File

@@ -29,11 +29,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{C8065AE3
Directory.Build.props = Directory.Build.props Directory.Build.props = Directory.Build.props
EndProjectSection EndProjectSection
EndProject 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 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 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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Proxy", "AMWD.Protocols.Modbus.Proxy\AMWD.Protocols.Modbus.Proxy.csproj", "{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution 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}.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.ActiveCfg = Release|Any CPU
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@@ -7,12 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
_nothing changed yet_ _no changes_
## [v0.3.0] (2024-05-31)
### Added
- New `AMWD.Protocols.Modbus.Proxy` package, that contains the server implementations as proxies
### Changed
- Renamed `ModbusSerialServer` to `ModbusRtuServer` to clearify the protocol that is used
- Made `Protocol` property of `ModbusClientBase` non-abstract
### Fixed
- Issue with missing client on TCP connection when using default constructor (seems that `AddressFamily.Unknown` caused the problem)
## [v0.2.0] (2024-04-02) ## [v0.2.0] (2024-04-02)
First "final" re-implementation. First "final" re-implementation
## v0.1.0 (2022-08-28) ## v0.1.0 (2022-08-28)
@@ -22,5 +38,6 @@ So this tag is only here for documentation purposes of the NuGet Gallery.
[Unreleased]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.2.0...HEAD [Unreleased]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.0...HEAD
[v0.3.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.2.0...v0.3.0
[v0.2.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/tree/v0.2.0 [v0.2.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/tree/v0.2.0

View File

@@ -4,7 +4,7 @@ Here you can find a basic implementation of the Modbus protocol.
## Overview ## Overview
The project is divided into three parts. The project is divided into four parts.
To be mentioned at the beginning: To be mentioned at the beginning:
Only the clients are build very modular to fit any requirement reached on the first implementation back in 2018 ([see here]). Only the clients are build very modular to fit any requirement reached on the first implementation back in 2018 ([see here]).
@@ -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. 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] ### [Serial]
This package contains some wrappers and implementations for the serial protocol. 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 [see here]: https://github.com/andreasAMmueller/Modbus
[Common]: AMWD.Protocols.Modbus.Common/README.md [Common]: AMWD.Protocols.Modbus.Common/README.md
[Proxy]: AMWD.Protocols.Modbus.Proxy/README.md
[Serial]: AMWD.Protocols.Modbus.Serial/README.md [Serial]: AMWD.Protocols.Modbus.Serial/README.md
[TCP]: AMWD.Protocols.Modbus.Tcp/README.md [TCP]: AMWD.Protocols.Modbus.Tcp/README.md
[MIT License]: LICENSE.txt [MIT License]: LICENSE.txt