Implemented Modbus Serial Client
This commit is contained in:
@@ -31,12 +31,12 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
TimeSpan ConnectTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive time out value of the connection.
|
||||
/// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a read/receive operation does not finish.
|
||||
/// </summary>
|
||||
TimeSpan ReadTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the send time out value of the connection.
|
||||
/// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a write/send operation does not finish.
|
||||
/// </summary>
|
||||
TimeSpan WriteTimeout { get; set; }
|
||||
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tcp.Events
|
||||
namespace AMWD.Protocols.Modbus.Common.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the coil written event arguments.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public class CoilWrittenEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unit id.
|
||||
/// </summary>
|
||||
public byte UnitId { get; internal set; }
|
||||
public byte UnitId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the coil address.
|
||||
/// </summary>
|
||||
public ushort Address { get; internal set; }
|
||||
public ushort Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the coil value.
|
||||
/// </summary>
|
||||
public bool Value { get; internal set; }
|
||||
public bool Value { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,36 @@
|
||||
using System;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tcp.Events
|
||||
namespace AMWD.Protocols.Modbus.Common.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the register written event arguments.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public class RegisterWrittenEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unit id.
|
||||
/// </summary>
|
||||
public byte UnitId { get; internal set; }
|
||||
public byte UnitId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the address of the register.
|
||||
/// </summary>
|
||||
public ushort Address { get; internal set; }
|
||||
public ushort Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the register.
|
||||
/// </summary>
|
||||
public ushort Value { get; internal set; }
|
||||
public ushort Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the high byte of the register.
|
||||
/// </summary>
|
||||
public byte HighByte { get; internal set; }
|
||||
public byte HighByte { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the low byte of the register.
|
||||
/// </summary>
|
||||
public byte LowByte { get; internal set; }
|
||||
public byte LowByte { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -10,15 +10,39 @@
|
||||
|
||||
<Product>Modbus RTU/ASCII Protocol</Product>
|
||||
<Description>Implementation of the Modbus protocol communicating via serial line using RTU or ASCII encoding.</Description>
|
||||
<PackageTags>Modbus Protocol Serial Line RTU ASCII COM TTY</PackageTags>
|
||||
<PackageTags>Modbus Protocol Serial Line RTU ASCII COM TTY USB</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="../AMWD.Protocols.Modbus.Common/InternalsVisibleTo.cs" Link="InternalsVisibleTo.cs" />
|
||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" />
|
||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ReaderWriterLockSlimExtensions.cs" Link="Extensions/ReaderWriterLockSlimExtensions.cs" />
|
||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/AsyncQueue.cs" Link="Utils/AsyncQueue.cs" />
|
||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/RequestQueueItem.cs" Link="Utils/RequestQueueItem.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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Extensions\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
43
AMWD.Protocols.Modbus.Serial/Enums/BaudRate.cs
Normal file
43
AMWD.Protocols.Modbus.Serial/Enums/BaudRate.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
namespace AMWD.Protocols.Modbus.Serial
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the baud rates for a serial connection.
|
||||
/// </summary>
|
||||
public enum BaudRate : int
|
||||
{
|
||||
/// <summary>
|
||||
/// 2400 Baud.
|
||||
/// </summary>
|
||||
Baud2400 = 2400,
|
||||
|
||||
/// <summary>
|
||||
/// 4800 Baud.
|
||||
/// </summary>
|
||||
Baud4800 = 4800,
|
||||
|
||||
/// <summary>
|
||||
/// 9600 Baud.
|
||||
/// </summary>
|
||||
Baud9600 = 9600,
|
||||
|
||||
/// <summary>
|
||||
/// 19200 Baud.
|
||||
/// </summary>
|
||||
Baud19200 = 19200,
|
||||
|
||||
/// <summary>
|
||||
/// 38400 Baud.
|
||||
/// </summary>
|
||||
Baud38400 = 38400,
|
||||
|
||||
/// <summary>
|
||||
/// 57600 Baud.
|
||||
/// </summary>
|
||||
Baud57600 = 57600,
|
||||
|
||||
/// <summary>
|
||||
/// 115200 Baud.
|
||||
/// </summary>
|
||||
Baud115200 = 115200
|
||||
}
|
||||
}
|
||||
31
AMWD.Protocols.Modbus.Serial/Enums/RS485Flags.cs
Normal file
31
AMWD.Protocols.Modbus.Serial/Enums/RS485Flags.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Serial.Enums
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the flags for the RS485 driver state.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
internal enum RS485Flags
|
||||
{
|
||||
/// <summary>
|
||||
/// RS485 is enabled.
|
||||
/// </summary>
|
||||
Enabled = 1,
|
||||
|
||||
/// <summary>
|
||||
/// RS485 uses RTS on send.
|
||||
/// </summary>
|
||||
RtsOnSend = 2,
|
||||
|
||||
/// <summary>
|
||||
/// RS485 uses RTS after send.
|
||||
/// </summary>
|
||||
RtsAfterSend = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Receive during send (duplex).
|
||||
/// </summary>
|
||||
RxDuringTx = 16
|
||||
}
|
||||
}
|
||||
83
AMWD.Protocols.Modbus.Serial/Exceptions/UnixIOException.cs
Normal file
83
AMWD.Protocols.Modbus.Serial/Exceptions/UnixIOException.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Serialization;
|
||||
using AMWD.Protocols.Modbus.Serial.Utils;
|
||||
#if NETSTANDARD
|
||||
using System.Security.Permissions;
|
||||
#endif
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Serial
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a unix specific IO exception.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See StackOverflow answer: <see href="https://stackoverflow.com/a/10388107"/>.
|
||||
/// </remarks>
|
||||
[Serializable]
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public class UnixIOException : ExternalException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UnixIOException"/> class.
|
||||
/// </summary>
|
||||
#if NETSTANDARD
|
||||
[SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
|
||||
#endif
|
||||
public UnixIOException()
|
||||
: this(Marshal.GetLastWin32Error())
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UnixIOException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="errorCode">The native error code.</param>
|
||||
#if NETSTANDARD
|
||||
[SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
|
||||
#endif
|
||||
public UnixIOException(int errorCode)
|
||||
: this(GetErrorMessage(errorCode), errorCode)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UnixIOException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
#if NETSTANDARD
|
||||
[SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
|
||||
#endif
|
||||
public UnixIOException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public UnixIOException(string message, Exception inner)
|
||||
: base(message, inner)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public UnixIOException(string message, int errorCode)
|
||||
: base(message, errorCode)
|
||||
{ }
|
||||
|
||||
#if ! NET8_0_OR_GREATER
|
||||
/// <inheritdoc/>
|
||||
protected UnixIOException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{ }
|
||||
#endif
|
||||
|
||||
private static string GetErrorMessage(int errorCode)
|
||||
{
|
||||
try
|
||||
{
|
||||
nint ptr = UnsafeNativeMethods.StrError(errorCode);
|
||||
return Marshal.PtrToStringAnsi(ptr);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return $"Unknown error: 0x{errorCode:x}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
230
AMWD.Protocols.Modbus.Serial/ModbusSerialClient.cs
Normal file
230
AMWD.Protocols.Modbus.Serial/ModbusSerialClient.cs
Normal file
@@ -0,0 +1,230 @@
|
||||
using System;
|
||||
using System.IO.Ports;
|
||||
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||
using AMWD.Protocols.Modbus.Common.Protocols;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Serial
|
||||
{
|
||||
/// <summary>
|
||||
/// Default implementation of a Modbus serial line client.
|
||||
/// </summary>
|
||||
public class ModbusSerialClient : ModbusClientBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModbusSerialClient"/> class with a port name.
|
||||
/// </summary>
|
||||
/// <param name="portName">The name of the serial port to use.</param>
|
||||
public ModbusSerialClient(string portName)
|
||||
: this(new ModbusSerialConnection { PortName = portName })
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModbusSerialClient"/> class with a specific <see cref="IModbusConnection"/>.
|
||||
/// </summary>
|
||||
/// <param name="connection">The <see cref="IModbusConnection"/> responsible for invoking the requests.</param>
|
||||
public ModbusSerialClient(IModbusConnection connection)
|
||||
: this(connection, true)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModbusSerialClient"/> class with a specific <see cref="IModbusConnection"/>.
|
||||
/// </summary>
|
||||
/// <param name="connection">The <see cref="IModbusConnection"/> responsible for invoking the requests.</param>
|
||||
/// <param name="disposeConnection">
|
||||
/// <see langword="true"/> if the connection should be disposed of by Dispose(),
|
||||
/// <see langword="false"/> otherwise if you inted to reuse the connection.
|
||||
/// </param>
|
||||
public ModbusSerialClient(IModbusConnection connection, bool disposeConnection)
|
||||
: base(connection, disposeConnection)
|
||||
{
|
||||
Protocol = new RtuProtocol();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.GetPortNames" />
|
||||
public static string[] AvailablePortNames => SerialPort.GetPortNames();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IModbusProtocol Protocol { get; set; }
|
||||
|
||||
/// <inheritdoc cref="IModbusConnection.IdleTimeout"/>
|
||||
public TimeSpan IdleTimeout
|
||||
{
|
||||
get => connection.IdleTimeout;
|
||||
set => connection.IdleTimeout = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IModbusConnection.ConnectTimeout"/>
|
||||
public TimeSpan ConnectTimeout
|
||||
{
|
||||
get => connection.ConnectTimeout;
|
||||
set => connection.ConnectTimeout = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IModbusConnection.ReadTimeout"/>
|
||||
public TimeSpan ReadTimeout
|
||||
{
|
||||
get => connection.ReadTimeout;
|
||||
set => connection.ReadTimeout = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IModbusConnection.WriteTimeout"/>
|
||||
public TimeSpan WriteTimeout
|
||||
{
|
||||
get => connection.WriteTimeout;
|
||||
set => connection.WriteTimeout = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ModbusSerialConnection.DriverEnabledRS485"/>
|
||||
public bool DriverEnabledRS485
|
||||
{
|
||||
get
|
||||
{
|
||||
if (connection is ModbusSerialConnection serialConnection)
|
||||
return serialConnection.DriverEnabledRS485;
|
||||
|
||||
return default;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (connection is ModbusSerialConnection serialConnection)
|
||||
serialConnection.DriverEnabledRS485 = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ModbusSerialConnection.InterRequestDelay"/>
|
||||
public TimeSpan InterRequestDelay
|
||||
{
|
||||
get
|
||||
{
|
||||
if (connection is ModbusSerialConnection serialConnection)
|
||||
return serialConnection.InterRequestDelay;
|
||||
|
||||
return default;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (connection is ModbusSerialConnection serialConnection)
|
||||
serialConnection.InterRequestDelay = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ModbusSerialConnection.PortName"/>
|
||||
public string PortName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (connection is ModbusSerialConnection serialConnection)
|
||||
return serialConnection.PortName;
|
||||
|
||||
return default;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (connection is ModbusSerialConnection serialConnection)
|
||||
serialConnection.PortName = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ModbusSerialConnection.BaudRate"/>
|
||||
public BaudRate BaudRate
|
||||
{
|
||||
get
|
||||
{
|
||||
if (connection is ModbusSerialConnection serialConnection)
|
||||
return serialConnection.BaudRate;
|
||||
|
||||
return default;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (connection is ModbusSerialConnection serialConnection)
|
||||
serialConnection.BaudRate = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ModbusSerialConnection.DataBits"/>
|
||||
public int DataBits
|
||||
{
|
||||
get
|
||||
{
|
||||
if (connection is ModbusSerialConnection serialConnection)
|
||||
return serialConnection.DataBits;
|
||||
|
||||
return default;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (connection is ModbusSerialConnection serialConnection)
|
||||
serialConnection.DataBits = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ModbusSerialConnection.Handshake"/>
|
||||
public Handshake Handshake
|
||||
{
|
||||
get
|
||||
{
|
||||
if (connection is ModbusSerialConnection serialConnection)
|
||||
return serialConnection.Handshake;
|
||||
|
||||
return default;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (connection is ModbusSerialConnection serialConnection)
|
||||
serialConnection.Handshake = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ModbusSerialConnection.Parity"/>
|
||||
public Parity Parity
|
||||
{
|
||||
get
|
||||
{
|
||||
if (connection is ModbusSerialConnection serialConnection)
|
||||
return serialConnection.Parity;
|
||||
|
||||
return default;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (connection is ModbusSerialConnection serialConnection)
|
||||
serialConnection.Parity = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ModbusSerialConnection.RtsEnable"/>
|
||||
public bool RtsEnable
|
||||
{
|
||||
get
|
||||
{
|
||||
if (connection is ModbusSerialConnection serialConnection)
|
||||
return serialConnection.RtsEnable;
|
||||
|
||||
return default;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (connection is ModbusSerialConnection serialConnection)
|
||||
serialConnection.RtsEnable = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ModbusSerialConnection.StopBits"/>
|
||||
public StopBits StopBits
|
||||
{
|
||||
get
|
||||
{
|
||||
if (connection is ModbusSerialConnection serialConnection)
|
||||
return serialConnection.StopBits;
|
||||
|
||||
return default;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (connection is ModbusSerialConnection serialConnection)
|
||||
serialConnection.StopBits = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
399
AMWD.Protocols.Modbus.Serial/ModbusSerialConnection.cs
Normal file
399
AMWD.Protocols.Modbus.Serial/ModbusSerialConnection.cs
Normal file
@@ -0,0 +1,399 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Ports;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||
using AMWD.Protocols.Modbus.Common.Protocols;
|
||||
using AMWD.Protocols.Modbus.Common.Utils;
|
||||
using AMWD.Protocols.Modbus.Serial.Enums;
|
||||
using AMWD.Protocols.Modbus.Serial.Utils;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Serial
|
||||
{
|
||||
/// <summary>
|
||||
/// The default Modbus Serial connection.
|
||||
/// </summary>
|
||||
public class ModbusSerialConnection : IModbusConnection
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private bool _isDisposed;
|
||||
private readonly CancellationTokenSource _disposeCts = new();
|
||||
|
||||
private readonly SemaphoreSlim _portLock = new(1, 1);
|
||||
private readonly SerialPortWrapper _serialPort;
|
||||
private readonly Timer _idleTimer;
|
||||
|
||||
private readonly Task _processingTask;
|
||||
private readonly AsyncQueue<RequestQueueItem> _requestQueue = new();
|
||||
|
||||
// Only required to cover all logic branches on unit tests.
|
||||
private bool _isUnitTest = false;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModbusSerialConnection"/> class.
|
||||
/// </summary>
|
||||
public ModbusSerialConnection()
|
||||
{
|
||||
_serialPort = new SerialPortWrapper
|
||||
{
|
||||
BaudRate = (int)BaudRate.Baud19200,
|
||||
DataBits = 8,
|
||||
Handshake = Handshake.None,
|
||||
Parity = Parity.Even,
|
||||
ReadTimeout = 1000,
|
||||
RtsEnable = false,
|
||||
StopBits = StopBits.One,
|
||||
WriteTimeout = 1000,
|
||||
};
|
||||
|
||||
_idleTimer = new Timer(OnIdleTimer);
|
||||
_processingTask = ProcessAsync(_disposeCts.Token);
|
||||
}
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "Serial";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual TimeSpan IdleTimeout { get; set; } = TimeSpan.FromSeconds(6);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual TimeSpan ConnectTimeout { get; set; } = TimeSpan.MaxValue;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual TimeSpan ReadTimeout
|
||||
{
|
||||
get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout);
|
||||
set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual TimeSpan WriteTimeout
|
||||
{
|
||||
get => TimeSpan.FromMilliseconds(_serialPort.WriteTimeout);
|
||||
set => _serialPort.WriteTimeout = (int)value.TotalMilliseconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the RS485 driver has to be enabled via software switch.
|
||||
/// </summary>
|
||||
public virtual bool DriverEnabledRS485 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a wait-time between requests.
|
||||
/// </summary>
|
||||
public virtual TimeSpan InterRequestDelay { get; set; } = TimeSpan.Zero;
|
||||
|
||||
#region SerialPort Properties
|
||||
|
||||
/// <inheritdoc cref="SerialPort.PortName" />
|
||||
public virtual string PortName
|
||||
{
|
||||
get => _serialPort.PortName;
|
||||
set => _serialPort.PortName = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the serial baud rate.
|
||||
/// </summary>
|
||||
public virtual BaudRate BaudRate
|
||||
{
|
||||
get => (BaudRate)_serialPort.BaudRate;
|
||||
set => _serialPort.BaudRate = (int)value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.DataBits" />
|
||||
/// <remarks>
|
||||
/// Should be 7 for ASCII mode and 8 for RTU mode.
|
||||
/// </remarks>
|
||||
public virtual int DataBits
|
||||
{
|
||||
get => _serialPort.DataBits;
|
||||
set => _serialPort.DataBits = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.Handshake" />
|
||||
public virtual Handshake Handshake
|
||||
{
|
||||
get => _serialPort.Handshake;
|
||||
set => _serialPort.Handshake = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.Parity" />
|
||||
/// <remarks>
|
||||
/// From the Specs:
|
||||
/// <br/>
|
||||
/// <see cref="Parity.Even"/> is recommended and therefore the default value.
|
||||
/// <br/>
|
||||
/// If you use <see cref="Parity.None"/>, <see cref="StopBits.Two"/> is required,
|
||||
/// otherwise <see cref="StopBits.One"/> should work fine.
|
||||
/// </remarks>
|
||||
public virtual Parity Parity
|
||||
{
|
||||
get => _serialPort.Parity;
|
||||
set => _serialPort.Parity = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.RtsEnable" />
|
||||
public virtual bool RtsEnable
|
||||
{
|
||||
get => _serialPort.RtsEnable;
|
||||
set => _serialPort.RtsEnable = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.StopBits" />
|
||||
/// <remarks>
|
||||
/// From the Specs:
|
||||
/// <br/>
|
||||
/// Should be <see cref="StopBits.One"/> for <see cref="Parity.Even"/> or <see cref="Parity.Odd"/> and
|
||||
/// <br/>
|
||||
/// should be <see cref="StopBits.Two"/> for <see cref="Parity.None"/>.
|
||||
/// </remarks>
|
||||
public virtual StopBits StopBits
|
||||
{
|
||||
get => _serialPort.StopBits;
|
||||
set => _serialPort.StopBits = value;
|
||||
}
|
||||
|
||||
#endregion SerialPort Properties
|
||||
|
||||
#endregion Properties
|
||||
|
||||
/// <summary>
|
||||
/// Releases all managed and unmanaged resources used by the <see cref="IModbusConnection"/>.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
return;
|
||||
|
||||
_isDisposed = true;
|
||||
_disposeCts.Cancel();
|
||||
|
||||
_idleTimer.Dispose();
|
||||
|
||||
try
|
||||
{
|
||||
_processingTask.Wait();
|
||||
_processingTask.Dispose();
|
||||
}
|
||||
catch
|
||||
{ /* keep it quiet */ }
|
||||
|
||||
OnIdleTimer(null);
|
||||
|
||||
_serialPort.Dispose();
|
||||
_portLock.Dispose();
|
||||
|
||||
while (_requestQueue.TryDequeue(out var item))
|
||||
{
|
||||
item.CancellationTokenRegistration.Dispose();
|
||||
item.CancellationTokenSource.Dispose();
|
||||
item.TaskCompletionSource.TrySetException(new ObjectDisposedException(GetType().FullName));
|
||||
}
|
||||
|
||||
_disposeCts.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#region Request processing
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<IReadOnlyList<byte>> InvokeAsync(IReadOnlyList<byte> request, Func<IReadOnlyList<byte>, bool> validateResponseComplete, CancellationToken cancellationToken = default)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ObjectDisposedException.ThrowIf(_isDisposed, this);
|
||||
#else
|
||||
if (_isDisposed)
|
||||
throw new ObjectDisposedException(GetType().FullName);
|
||||
#endif
|
||||
|
||||
if (request == null || request.Count < 1)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(validateResponseComplete);
|
||||
#else
|
||||
if (validateResponseComplete == null)
|
||||
throw new ArgumentNullException(nameof(validateResponseComplete));
|
||||
#endif
|
||||
|
||||
var item = new RequestQueueItem
|
||||
{
|
||||
Request = [.. request],
|
||||
ValidateResponseComplete = validateResponseComplete,
|
||||
TaskCompletionSource = new(),
|
||||
CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)
|
||||
};
|
||||
|
||||
item.CancellationTokenRegistration = item.CancellationTokenSource.Token.Register(() =>
|
||||
{
|
||||
_requestQueue.Remove(item);
|
||||
item.CancellationTokenSource.Dispose();
|
||||
item.TaskCompletionSource.TrySetCanceled(cancellationToken);
|
||||
item.CancellationTokenRegistration.Dispose();
|
||||
});
|
||||
|
||||
_requestQueue.Enqueue(item);
|
||||
return item.TaskCompletionSource.Task;
|
||||
}
|
||||
|
||||
private async Task ProcessAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get next request to process
|
||||
var item = await _requestQueue.DequeueAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Remove registration => already removed from queue
|
||||
item.CancellationTokenRegistration.Dispose();
|
||||
|
||||
// Build combined cancellation token
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
|
||||
// Wait for exclusive access
|
||||
await _portLock.WaitAsync(linkedCts.Token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
// Ensure connection is up
|
||||
await AssertConnection(linkedCts.Token).ConfigureAwait(false);
|
||||
|
||||
await _serialPort.WriteAsync(item.Request, linkedCts.Token).ConfigureAwait(false);
|
||||
|
||||
linkedCts.Token.ThrowIfCancellationRequested();
|
||||
|
||||
var bytes = new List<byte>();
|
||||
byte[] buffer = new byte[RtuProtocol.MAX_ADU_LENGTH];
|
||||
|
||||
do
|
||||
{
|
||||
int readCount = await _serialPort.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token).ConfigureAwait(false);
|
||||
if (readCount < 1)
|
||||
throw new EndOfStreamException();
|
||||
|
||||
bytes.AddRange(buffer.Take(readCount));
|
||||
|
||||
linkedCts.Token.ThrowIfCancellationRequested();
|
||||
}
|
||||
while (!item.ValidateResponseComplete(bytes));
|
||||
|
||||
item.TaskCompletionSource.TrySetResult(bytes);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Dispose() called
|
||||
item.TaskCompletionSource.TrySetCanceled(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (item.CancellationTokenSource.IsCancellationRequested)
|
||||
{
|
||||
// Cancellation requested by user
|
||||
item.TaskCompletionSource.TrySetCanceled(item.CancellationTokenSource.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
item.TaskCompletionSource.TrySetException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_portLock.Release();
|
||||
_idleTimer.Change(IdleTimeout, Timeout.InfiniteTimeSpan);
|
||||
|
||||
await Task.Delay(InterRequestDelay, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Dispose() called while waiting for request item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Request processing
|
||||
|
||||
#region Connection handling
|
||||
|
||||
// Has to be called within _portLock!
|
||||
private async Task AssertConnection(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_serialPort.IsOpen)
|
||||
return;
|
||||
|
||||
int delay = 1;
|
||||
int maxDelay = 60;
|
||||
|
||||
var startTime = DateTime.UtcNow;
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
_serialPort.Close();
|
||||
_serialPort.ResetRS485DriverStateFlags();
|
||||
|
||||
if (DriverEnabledRS485 && (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || _isUnitTest))
|
||||
{
|
||||
var flags = _serialPort.GetRS485DriverStateFlags();
|
||||
flags |= RS485Flags.Enabled;
|
||||
flags &= ~RS485Flags.RxDuringTx;
|
||||
_serialPort.ChangeRS485DriverStateFlags(flags);
|
||||
}
|
||||
|
||||
using var connectTask = Task.Run(_serialPort.Open);
|
||||
if (await Task.WhenAny(connectTask, Task.Delay(ReadTimeout, cancellationToken)) == connectTask)
|
||||
{
|
||||
await connectTask;
|
||||
if (_serialPort.IsOpen)
|
||||
return;
|
||||
}
|
||||
|
||||
throw new IOException();
|
||||
}
|
||||
catch (IOException) when (ConnectTimeout == TimeSpan.MaxValue || DateTime.UtcNow.Subtract(startTime) < ConnectTimeout)
|
||||
{
|
||||
delay *= 2;
|
||||
if (delay > maxDelay)
|
||||
delay = maxDelay;
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{ /* keep it quiet */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnIdleTimer(object _)
|
||||
{
|
||||
try
|
||||
{
|
||||
_portLock.Wait(_disposeCts.Token);
|
||||
try
|
||||
{
|
||||
if (!_serialPort.IsOpen)
|
||||
return;
|
||||
|
||||
_serialPort.Close();
|
||||
_serialPort.ResetRS485DriverStateFlags();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_portLock.Release();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{ /* keep it quiet */ }
|
||||
}
|
||||
|
||||
#endregion Connection handling
|
||||
}
|
||||
}
|
||||
38
AMWD.Protocols.Modbus.Serial/Utils/SafeUnixHandle.cs
Normal file
38
AMWD.Protocols.Modbus.Serial/Utils/SafeUnixHandle.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
#if NETSTANDARD
|
||||
using System.Runtime.ConstrainedExecution;
|
||||
using System.Security.Permissions;
|
||||
#endif
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Serial.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a safe handle for unix systems.
|
||||
/// <br/>
|
||||
/// Found on https://stackoverflow.com/a/10388107
|
||||
/// </summary>
|
||||
#if NETSTANDARD
|
||||
[SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode = true)]
|
||||
[SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
|
||||
#endif
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal sealed class SafeUnixHandle : SafeHandle
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SafeUnixHandle"/> class.
|
||||
/// </summary>
|
||||
#if NETSTANDARD
|
||||
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
|
||||
#endif
|
||||
private SafeUnixHandle()
|
||||
: base(new IntPtr(-1), true)
|
||||
{ }
|
||||
|
||||
public override bool IsInvalid
|
||||
=> handle == new IntPtr(-1);
|
||||
|
||||
protected override bool ReleaseHandle()
|
||||
=> UnsafeNativeMethods.Close(handle) != -1;
|
||||
}
|
||||
}
|
||||
291
AMWD.Protocols.Modbus.Serial/Utils/SerialPortWrapper.cs
Normal file
291
AMWD.Protocols.Modbus.Serial/Utils/SerialPortWrapper.cs
Normal file
@@ -0,0 +1,291 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Ports;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using AMWD.Protocols.Modbus.Serial.Enums;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Serial.Utils
|
||||
{
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal class SerialPortWrapper : IDisposable
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly SerialPort _serialPort = new();
|
||||
|
||||
private bool _driverStateChanged = false;
|
||||
private RS485Flags _initialFlags = 0;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <inheritdoc cref="SerialPort.Handshake"/>
|
||||
public virtual Handshake Handshake
|
||||
{
|
||||
get => _serialPort.Handshake;
|
||||
set => _serialPort.Handshake = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.DataBits"/>
|
||||
public virtual int DataBits
|
||||
{
|
||||
get => _serialPort.DataBits;
|
||||
set => _serialPort.DataBits = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.IsOpen"/>
|
||||
public virtual bool IsOpen
|
||||
=> _serialPort.IsOpen;
|
||||
|
||||
/// <inheritdoc cref="SerialPort.PortName"/>
|
||||
public virtual string PortName
|
||||
{
|
||||
get => _serialPort.PortName;
|
||||
set => _serialPort.PortName = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.ReadTimeout"/>
|
||||
public virtual int ReadTimeout
|
||||
{
|
||||
get => _serialPort.ReadTimeout;
|
||||
set => _serialPort.ReadTimeout = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.RtsEnable"/>
|
||||
public virtual bool RtsEnable
|
||||
{
|
||||
get => _serialPort.RtsEnable;
|
||||
set => _serialPort.RtsEnable = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.StopBits"/>
|
||||
public virtual StopBits StopBits
|
||||
{
|
||||
get => _serialPort.StopBits;
|
||||
set => _serialPort.StopBits = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.WriteTimeout"/>
|
||||
public virtual int WriteTimeout
|
||||
{
|
||||
get => _serialPort.WriteTimeout;
|
||||
set => _serialPort.WriteTimeout = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.Parity"/>
|
||||
public virtual Parity Parity
|
||||
{
|
||||
get => _serialPort.Parity;
|
||||
set => _serialPort.Parity = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.BaudRate"/>
|
||||
public virtual int BaudRate
|
||||
{
|
||||
get => _serialPort.BaudRate;
|
||||
set => _serialPort.BaudRate = value;
|
||||
}
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <inheritdoc cref="SerialPort.Close"/>
|
||||
public virtual void Close()
|
||||
=> _serialPort.Close();
|
||||
|
||||
/// <inheritdoc cref="SerialPort.Open"/>
|
||||
public virtual void Open()
|
||||
=> _serialPort.Open();
|
||||
|
||||
/// <inheritdoc cref="SerialPort.Dispose"/>
|
||||
public virtual void Dispose()
|
||||
=> _serialPort.Dispose();
|
||||
|
||||
#endregion Methods
|
||||
|
||||
#region Extensions
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously reads a sequence of bytes from the current serial port, advances the
|
||||
/// position within the stream by the number of bytes read, and monitors cancellation
|
||||
/// requests.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// There seems to be a bug with the async stream implementation on Windows.
|
||||
/// <br/>
|
||||
/// See this StackOverflow answer: <see href="https://stackoverflow.com/a/54610437/11906695" />
|
||||
/// </remarks>
|
||||
/// <param name="buffer">The buffer to write the data into.</param>
|
||||
/// <param name="offset">The byte offset in buffer at which to begin writing data from the serial port.</param>
|
||||
/// <param name="count">The maximum number of bytes to read.</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>
|
||||
/// A task that represents the asynchronous read operation. The value of the TResult
|
||||
/// parameter contains the total number of bytes read into the buffer. The result
|
||||
/// value can be less than the number of bytes requested if the number of bytes currently
|
||||
/// available is less than the requested number, or it can be 0 (zero) if the end
|
||||
/// of the stream has been reached.
|
||||
/// </returns>
|
||||
public virtual async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var cts = new CancellationTokenSource(_serialPort.ReadTimeout);
|
||||
using var reg = cancellationToken.Register(cts.Cancel);
|
||||
|
||||
var ctr = default(CancellationTokenRegistration);
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// The async stream implementation on windows seems a bit broken.
|
||||
// So this will ensure the task to return to the caller.
|
||||
ctr = cts.Token.Register(_serialPort.DiscardInBuffer);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await _serialPort.BaseStream.ReadAsync(buffer, offset, count, cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return 0;
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
{
|
||||
throw new TimeoutException("No bytes read within the ReadTimeout.");
|
||||
}
|
||||
catch (IOException) when (cts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw new TimeoutException("No bytes read within the ReadTimeout.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
ctr.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously writes a sequence of bytes to the current serial port, advances the
|
||||
/// current position within this stream by the number of bytes written, and monitors
|
||||
/// cancellation requests.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// There seems to be a bug with the async stream implementation on Windows.
|
||||
/// <br/>
|
||||
/// See this StackOverflow answer: <see href="https://stackoverflow.com/a/54610437/11906695" />
|
||||
/// </remarks>
|
||||
/// <param name="buffer">The buffer to write the data from.</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task WriteAsync(byte[] buffer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var cts = new CancellationTokenSource(_serialPort.WriteTimeout);
|
||||
using var reg = cancellationToken.Register(cts.Cancel);
|
||||
|
||||
var ctr = default(CancellationTokenRegistration);
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// The async stream implementation on windows seems a bit broken.
|
||||
// So this will ensure the task to return to the caller.
|
||||
ctr = cts.Token.Register(_serialPort.DiscardOutBuffer);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
await _serialPort.BaseStream.WriteAsync(buffer, cts.Token).ConfigureAwait(false);
|
||||
#else
|
||||
await _serialPort.BaseStream.WriteAsync(buffer, 0, buffer.Length, cts.Token).ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
{
|
||||
throw new TimeoutException("No bytes written within the WriteTimeout.");
|
||||
}
|
||||
catch (IOException) when (cts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw new TimeoutException("No bytes written within the WriteTimeout.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
ctr.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
internal virtual void ChangeRS485DriverStateFlags(RS485Flags flags)
|
||||
{
|
||||
if (_driverStateChanged)
|
||||
throw new InvalidOperationException("The RS485 driver state has already been changed.");
|
||||
|
||||
_driverStateChanged = true;
|
||||
_initialFlags = GetRS485DriverStateFlags();
|
||||
ChangeRS485DriverStateFlagsInternal(flags);
|
||||
}
|
||||
|
||||
internal virtual void ResetRS485DriverStateFlags()
|
||||
{
|
||||
if (!_driverStateChanged)
|
||||
return;
|
||||
|
||||
ChangeRS485DriverStateFlagsInternal(_initialFlags);
|
||||
_driverStateChanged = false;
|
||||
_initialFlags = 0;
|
||||
}
|
||||
|
||||
internal virtual RS485Flags GetRS485DriverStateFlags()
|
||||
{
|
||||
var rs485 = new SerialRS485();
|
||||
SafeUnixHandle handle = null;
|
||||
|
||||
try
|
||||
{
|
||||
handle = UnsafeNativeMethods.Open(PortName, UnsafeNativeMethods.O_RDWR | UnsafeNativeMethods.O_NOCTTY);
|
||||
if (UnsafeNativeMethods.IoCtl(handle, UnsafeNativeMethods.TIOCGRS485, ref rs485) == -1)
|
||||
throw new UnixIOException();
|
||||
}
|
||||
finally
|
||||
{
|
||||
handle?.Dispose();
|
||||
}
|
||||
|
||||
return rs485.Flags;
|
||||
}
|
||||
|
||||
private void ChangeRS485DriverStateFlagsInternal(RS485Flags flags)
|
||||
{
|
||||
var rs485 = new SerialRS485();
|
||||
SafeUnixHandle handle = null;
|
||||
|
||||
try
|
||||
{
|
||||
handle = UnsafeNativeMethods.Open(PortName, UnsafeNativeMethods.O_RDWR | UnsafeNativeMethods.O_NOCTTY);
|
||||
if (UnsafeNativeMethods.IoCtl(handle, UnsafeNativeMethods.TIOCGRS485, ref rs485) == -1)
|
||||
throw new UnixIOException();
|
||||
}
|
||||
finally
|
||||
{
|
||||
handle?.Dispose();
|
||||
}
|
||||
|
||||
rs485.Flags = flags;
|
||||
try
|
||||
{
|
||||
handle = UnsafeNativeMethods.Open(PortName, UnsafeNativeMethods.O_RDWR | UnsafeNativeMethods.O_NOCTTY);
|
||||
if (UnsafeNativeMethods.IoCtl(handle, UnsafeNativeMethods.TIOCSRS485, ref rs485) == -1)
|
||||
throw new UnixIOException();
|
||||
}
|
||||
finally
|
||||
{
|
||||
handle?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Extensions
|
||||
}
|
||||
}
|
||||
28
AMWD.Protocols.Modbus.Serial/Utils/SerialRS485.cs
Normal file
28
AMWD.Protocols.Modbus.Serial/Utils/SerialRS485.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using AMWD.Protocols.Modbus.Serial.Enums;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Serial.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the structure of the driver settings for RS485.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 32)]
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal struct SerialRS485
|
||||
{
|
||||
/// <summary>
|
||||
/// The flags to change the driver state.
|
||||
/// </summary>
|
||||
public RS485Flags Flags;
|
||||
|
||||
/// <summary>
|
||||
/// The delay in milliseconds before send.
|
||||
/// </summary>
|
||||
public uint RtsDelayBeforeSend;
|
||||
|
||||
/// <summary>
|
||||
/// The delay in milliseconds after send.
|
||||
/// </summary>
|
||||
public uint RtsDelayAfterSend;
|
||||
}
|
||||
}
|
||||
71
AMWD.Protocols.Modbus.Serial/Utils/UnsafeNativeMethods.cs
Normal file
71
AMWD.Protocols.Modbus.Serial/Utils/UnsafeNativeMethods.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
#if NETSTANDARD
|
||||
using System.Runtime.ConstrainedExecution;
|
||||
#endif
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Serial.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Definitions of the unsafe system methods.
|
||||
/// <br/>
|
||||
/// Found on https://stackoverflow.com/a/10388107
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal static class UnsafeNativeMethods
|
||||
{
|
||||
/// <summary>
|
||||
/// A flag for <see cref="Open(string, uint)"/>.
|
||||
/// </summary>
|
||||
internal const int O_RDWR = 2;
|
||||
|
||||
/// <summary>
|
||||
/// A flag for <see cref="Open(string, uint)"/>.
|
||||
/// </summary>
|
||||
internal const int O_NOCTTY = 256;
|
||||
|
||||
/// <summary>
|
||||
/// A flag for <see cref="IoCtl(SafeUnixHandle, uint, ref SerialRS485)"/>.
|
||||
/// </summary>
|
||||
internal const uint TIOCGRS485 = 0x542E;
|
||||
|
||||
/// <summary>
|
||||
/// A flag for <see cref="IoCtl(SafeUnixHandle, uint, ref SerialRS485)"/>.
|
||||
/// </summary>
|
||||
internal const uint TIOCSRS485 = 0x542F;
|
||||
|
||||
/// <summary>
|
||||
/// Opens a handle to a defined path (serial port).
|
||||
/// </summary>
|
||||
/// <param name="path">The path to open the handle.</param>
|
||||
/// <param name="flag">The flags for the handle.</param>
|
||||
[DllImport("libc", EntryPoint = "open", SetLastError = true)]
|
||||
internal static extern SafeUnixHandle Open(string path, uint flag);
|
||||
|
||||
/// <summary>
|
||||
/// Performs an ioctl request to the open handle.
|
||||
/// </summary>
|
||||
/// <param name="handle">The handle.</param>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <param name="serialRs485">The serial rs485 data structure to use.</param>
|
||||
[DllImport("libc", EntryPoint = "ioctl", SetLastError = true)]
|
||||
internal static extern int IoCtl(SafeUnixHandle handle, uint request, ref SerialRS485 serialRs485);
|
||||
|
||||
/// <summary>
|
||||
/// Closes an open handle.
|
||||
/// </summary>
|
||||
/// <param name="handle">The handle.</param>
|
||||
#if NETSTANDARD
|
||||
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
|
||||
#endif
|
||||
[DllImport("libc", EntryPoint = "close", SetLastError = true)]
|
||||
internal static extern int Close(IntPtr handle);
|
||||
|
||||
/// <summary>
|
||||
/// Converts the given error number (errno) into a readable string.
|
||||
/// </summary>
|
||||
/// <param name="errno">The error number.</param>
|
||||
[DllImport("libc", EntryPoint = "strerror", SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
|
||||
internal static extern IntPtr StrError(int errno);
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,9 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Protocols.Modbus.Common;
|
||||
using AMWD.Protocols.Modbus.Common.Events;
|
||||
using AMWD.Protocols.Modbus.Common.Models;
|
||||
using AMWD.Protocols.Modbus.Common.Protocols;
|
||||
using AMWD.Protocols.Modbus.Tcp.Events;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tcp
|
||||
{
|
||||
@@ -45,8 +45,6 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public ModbusTcpServer(IPAddress listenAddress = null, int listenPort = 502)
|
||||
{
|
||||
ListenAddress = listenAddress ?? IPAddress.Loopback;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj" />
|
||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
235
AMWD.Protocols.Modbus.Tests/Serial/ModbusSerialClientTest.cs
Normal file
235
AMWD.Protocols.Modbus.Tests/Serial/ModbusSerialClientTest.cs
Normal file
@@ -0,0 +1,235 @@
|
||||
using System.IO.Ports;
|
||||
using AMWD.Protocols.Modbus.Serial;
|
||||
using Moq;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tests.Serial
|
||||
{
|
||||
[TestClass]
|
||||
public class ModbusSerialClientTest
|
||||
{
|
||||
private Mock<IModbusConnection> _genericConnectionMock;
|
||||
private Mock<ModbusSerialConnection> _serialConnectionMock;
|
||||
|
||||
[TestInitialize]
|
||||
public void Initialize()
|
||||
{
|
||||
_genericConnectionMock = new Mock<IModbusConnection>();
|
||||
_genericConnectionMock.Setup(c => c.IdleTimeout).Returns(TimeSpan.FromSeconds(40));
|
||||
_genericConnectionMock.Setup(c => c.ConnectTimeout).Returns(TimeSpan.FromSeconds(30));
|
||||
_genericConnectionMock.Setup(c => c.ReadTimeout).Returns(TimeSpan.FromSeconds(20));
|
||||
_genericConnectionMock.Setup(c => c.WriteTimeout).Returns(TimeSpan.FromSeconds(10));
|
||||
|
||||
_serialConnectionMock = new Mock<ModbusSerialConnection>();
|
||||
|
||||
_serialConnectionMock.Setup(c => c.IdleTimeout).Returns(TimeSpan.FromSeconds(10));
|
||||
_serialConnectionMock.Setup(c => c.ConnectTimeout).Returns(TimeSpan.FromSeconds(20));
|
||||
_serialConnectionMock.Setup(c => c.ReadTimeout).Returns(TimeSpan.FromSeconds(30));
|
||||
_serialConnectionMock.Setup(c => c.WriteTimeout).Returns(TimeSpan.FromSeconds(40));
|
||||
|
||||
_serialConnectionMock.Setup(c => c.DriverEnabledRS485).Returns(true);
|
||||
_serialConnectionMock.Setup(c => c.InterRequestDelay).Returns(TimeSpan.FromSeconds(50));
|
||||
_serialConnectionMock.Setup(c => c.PortName).Returns("COM-42");
|
||||
_serialConnectionMock.Setup(c => c.BaudRate).Returns(BaudRate.Baud2400);
|
||||
_serialConnectionMock.Setup(c => c.DataBits).Returns(7);
|
||||
_serialConnectionMock.Setup(c => c.Handshake).Returns(Handshake.XOnXOff);
|
||||
_serialConnectionMock.Setup(c => c.Parity).Returns(Parity.Space);
|
||||
_serialConnectionMock.Setup(c => c.RtsEnable).Returns(true);
|
||||
_serialConnectionMock.Setup(c => c.StopBits).Returns(StopBits.OnePointFive);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldReturnDefaultValuesForGenericConnection()
|
||||
{
|
||||
// Arrange
|
||||
var client = new ModbusSerialClient(_genericConnectionMock.Object);
|
||||
|
||||
// Act
|
||||
bool driverEnabled = client.DriverEnabledRS485;
|
||||
var requestDelay = client.InterRequestDelay;
|
||||
string portName = client.PortName;
|
||||
var baudRate = client.BaudRate;
|
||||
int dataBits = client.DataBits;
|
||||
var handshake = client.Handshake;
|
||||
var parity = client.Parity;
|
||||
bool rtsEnable = client.RtsEnable;
|
||||
var stopBits = client.StopBits;
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(driverEnabled);
|
||||
Assert.AreEqual(TimeSpan.Zero, requestDelay);
|
||||
Assert.IsNull(portName);
|
||||
Assert.AreEqual(0, (int)baudRate);
|
||||
Assert.AreEqual(0, dataBits);
|
||||
Assert.AreEqual(0, (int)handshake);
|
||||
Assert.AreEqual(0, (int)parity);
|
||||
Assert.IsFalse(rtsEnable);
|
||||
Assert.AreEqual(0, (int)stopBits);
|
||||
|
||||
_genericConnectionMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldNotSetValuesForGenericConnection()
|
||||
{
|
||||
// Arrange
|
||||
var client = new ModbusSerialClient(_genericConnectionMock.Object);
|
||||
|
||||
// Act
|
||||
client.DriverEnabledRS485 = true;
|
||||
client.InterRequestDelay = TimeSpan.FromSeconds(123);
|
||||
client.PortName = "COM-42";
|
||||
client.BaudRate = BaudRate.Baud2400;
|
||||
client.DataBits = 7;
|
||||
client.Handshake = Handshake.XOnXOff;
|
||||
client.Parity = Parity.Space;
|
||||
client.RtsEnable = true;
|
||||
client.StopBits = StopBits.OnePointFive;
|
||||
|
||||
// Assert
|
||||
_genericConnectionMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldReturnValuesForGenericConnection()
|
||||
{
|
||||
// Arrange
|
||||
var client = new ModbusSerialClient(_genericConnectionMock.Object);
|
||||
|
||||
// Act
|
||||
var idleTimeout = client.IdleTimeout;
|
||||
var connectTimeout = client.ConnectTimeout;
|
||||
var readTimeout = client.ReadTimeout;
|
||||
var writeTimeout = client.WriteTimeout;
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(TimeSpan.FromSeconds(40), idleTimeout);
|
||||
Assert.AreEqual(TimeSpan.FromSeconds(30), connectTimeout);
|
||||
Assert.AreEqual(TimeSpan.FromSeconds(20), readTimeout);
|
||||
Assert.AreEqual(TimeSpan.FromSeconds(10), writeTimeout);
|
||||
|
||||
_genericConnectionMock.VerifyGet(c => c.IdleTimeout, Times.Once);
|
||||
_genericConnectionMock.VerifyGet(c => c.ConnectTimeout, Times.Once);
|
||||
_genericConnectionMock.VerifyGet(c => c.ReadTimeout, Times.Once);
|
||||
_genericConnectionMock.VerifyGet(c => c.WriteTimeout, Times.Once);
|
||||
_genericConnectionMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldSetValuesForGenericConnection()
|
||||
{
|
||||
// Arrange
|
||||
var client = new ModbusSerialClient(_genericConnectionMock.Object);
|
||||
|
||||
// Act
|
||||
client.IdleTimeout = TimeSpan.FromSeconds(10);
|
||||
client.ConnectTimeout = TimeSpan.FromSeconds(20);
|
||||
client.ReadTimeout = TimeSpan.FromSeconds(30);
|
||||
client.WriteTimeout = TimeSpan.FromSeconds(40);
|
||||
|
||||
// Assert
|
||||
_genericConnectionMock.VerifySet(c => c.IdleTimeout = TimeSpan.FromSeconds(10), Times.Once);
|
||||
_genericConnectionMock.VerifySet(c => c.ConnectTimeout = TimeSpan.FromSeconds(20), Times.Once);
|
||||
_genericConnectionMock.VerifySet(c => c.ReadTimeout = TimeSpan.FromSeconds(30), Times.Once);
|
||||
_genericConnectionMock.VerifySet(c => c.WriteTimeout = TimeSpan.FromSeconds(40), Times.Once);
|
||||
|
||||
_genericConnectionMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetValuesForSerialConnection()
|
||||
{
|
||||
// Arrange
|
||||
var client = new ModbusSerialClient(_serialConnectionMock.Object);
|
||||
|
||||
// Act
|
||||
bool driverEnabled = client.DriverEnabledRS485;
|
||||
var requestDelay = client.InterRequestDelay;
|
||||
string portName = client.PortName;
|
||||
var baudRate = client.BaudRate;
|
||||
int dataBits = client.DataBits;
|
||||
var handshake = client.Handshake;
|
||||
var parity = client.Parity;
|
||||
bool rtsEnable = client.RtsEnable;
|
||||
var stopBits = client.StopBits;
|
||||
|
||||
var idleTimeout = client.IdleTimeout;
|
||||
var connectTimeout = client.ConnectTimeout;
|
||||
var readTimeout = client.ReadTimeout;
|
||||
var writeTimeout = client.WriteTimeout;
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(driverEnabled);
|
||||
Assert.AreEqual(TimeSpan.FromSeconds(50), requestDelay);
|
||||
Assert.AreEqual("COM-42", portName);
|
||||
Assert.AreEqual(BaudRate.Baud2400, baudRate);
|
||||
Assert.AreEqual(7, dataBits);
|
||||
Assert.AreEqual(Handshake.XOnXOff, handshake);
|
||||
Assert.AreEqual(Parity.Space, parity);
|
||||
Assert.IsTrue(rtsEnable);
|
||||
Assert.AreEqual(StopBits.OnePointFive, stopBits);
|
||||
|
||||
Assert.AreEqual(TimeSpan.FromSeconds(10), idleTimeout);
|
||||
Assert.AreEqual(TimeSpan.FromSeconds(20), connectTimeout);
|
||||
Assert.AreEqual(TimeSpan.FromSeconds(30), readTimeout);
|
||||
Assert.AreEqual(TimeSpan.FromSeconds(40), writeTimeout);
|
||||
|
||||
_serialConnectionMock.VerifyGet(c => c.DriverEnabledRS485, Times.Once);
|
||||
_serialConnectionMock.VerifyGet(c => c.InterRequestDelay, Times.Once);
|
||||
_serialConnectionMock.VerifyGet(c => c.PortName, Times.Once);
|
||||
_serialConnectionMock.VerifyGet(c => c.BaudRate, Times.Once);
|
||||
_serialConnectionMock.VerifyGet(c => c.DataBits, Times.Once);
|
||||
_serialConnectionMock.VerifyGet(c => c.Handshake, Times.Once);
|
||||
_serialConnectionMock.VerifyGet(c => c.Parity, Times.Once);
|
||||
_serialConnectionMock.VerifyGet(c => c.RtsEnable, Times.Once);
|
||||
_serialConnectionMock.VerifyGet(c => c.StopBits, Times.Once);
|
||||
|
||||
_serialConnectionMock.VerifyGet(c => c.IdleTimeout, Times.Once);
|
||||
_serialConnectionMock.VerifyGet(c => c.ConnectTimeout, Times.Once);
|
||||
_serialConnectionMock.VerifyGet(c => c.ReadTimeout, Times.Once);
|
||||
_serialConnectionMock.VerifyGet(c => c.WriteTimeout, Times.Once);
|
||||
|
||||
_serialConnectionMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldSetValuesForSerialConnection()
|
||||
{
|
||||
// Arrange
|
||||
var client = new ModbusSerialClient(_serialConnectionMock.Object);
|
||||
|
||||
// Act
|
||||
client.DriverEnabledRS485 = true;
|
||||
client.InterRequestDelay = TimeSpan.FromSeconds(123);
|
||||
client.PortName = "COM-42";
|
||||
client.BaudRate = BaudRate.Baud2400;
|
||||
client.DataBits = 7;
|
||||
client.Handshake = Handshake.XOnXOff;
|
||||
client.Parity = Parity.Space;
|
||||
client.RtsEnable = true;
|
||||
client.StopBits = StopBits.OnePointFive;
|
||||
|
||||
client.IdleTimeout = TimeSpan.FromSeconds(40);
|
||||
client.ConnectTimeout = TimeSpan.FromSeconds(30);
|
||||
client.ReadTimeout = TimeSpan.FromSeconds(20);
|
||||
client.WriteTimeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
// Assert
|
||||
_serialConnectionMock.VerifySet(c => c.DriverEnabledRS485 = true, Times.Once);
|
||||
_serialConnectionMock.VerifySet(c => c.InterRequestDelay = TimeSpan.FromSeconds(123), Times.Once);
|
||||
_serialConnectionMock.VerifySet(c => c.PortName = "COM-42", Times.Once);
|
||||
_serialConnectionMock.VerifySet(c => c.BaudRate = BaudRate.Baud2400, Times.Once);
|
||||
_serialConnectionMock.VerifySet(c => c.DataBits = 7, Times.Once);
|
||||
_serialConnectionMock.VerifySet(c => c.Handshake = Handshake.XOnXOff, Times.Once);
|
||||
_serialConnectionMock.VerifySet(c => c.Parity = Parity.Space, Times.Once);
|
||||
_serialConnectionMock.VerifySet(c => c.RtsEnable = true, Times.Once);
|
||||
_serialConnectionMock.VerifySet(c => c.StopBits = StopBits.OnePointFive, Times.Once);
|
||||
|
||||
_serialConnectionMock.VerifySet(c => c.IdleTimeout = TimeSpan.FromSeconds(40), Times.Once);
|
||||
_serialConnectionMock.VerifySet(c => c.ConnectTimeout = TimeSpan.FromSeconds(30), Times.Once);
|
||||
_serialConnectionMock.VerifySet(c => c.ReadTimeout = TimeSpan.FromSeconds(20), Times.Once);
|
||||
_serialConnectionMock.VerifySet(c => c.WriteTimeout = TimeSpan.FromSeconds(10), Times.Once);
|
||||
|
||||
_serialConnectionMock.VerifyNoOtherCalls();
|
||||
}
|
||||
}
|
||||
}
|
||||
483
AMWD.Protocols.Modbus.Tests/Serial/ModbusSerialConnectionTest.cs
Normal file
483
AMWD.Protocols.Modbus.Tests/Serial/ModbusSerialConnectionTest.cs
Normal file
@@ -0,0 +1,483 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Ports;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Protocols.Modbus.Serial;
|
||||
using AMWD.Protocols.Modbus.Serial.Enums;
|
||||
using AMWD.Protocols.Modbus.Serial.Utils;
|
||||
using Moq;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tests.Serial
|
||||
{
|
||||
[TestClass]
|
||||
public class ModbusSerialConnectionTest
|
||||
{
|
||||
private Mock<SerialPortWrapper> _serialPortMock;
|
||||
|
||||
private bool _alwaysOpen;
|
||||
private Queue<bool> _isOpenQueue;
|
||||
|
||||
private readonly int _serialPortReadTimeout = 1000;
|
||||
private readonly int _serialPortWriteTimeout = 1000;
|
||||
|
||||
private List<byte[]> _serialLineRequestCallbacks;
|
||||
|
||||
private Queue<byte[]> _serialLineResponseQueue;
|
||||
|
||||
[TestInitialize]
|
||||
public void Initialize()
|
||||
{
|
||||
_alwaysOpen = true;
|
||||
_isOpenQueue = new Queue<bool>();
|
||||
|
||||
_serialLineRequestCallbacks = [];
|
||||
_serialLineResponseQueue = new Queue<byte[]>();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldGetAndSetPropertiesOfBaseClient()
|
||||
{
|
||||
// Arrange
|
||||
var connection = GetSerialConnection();
|
||||
|
||||
// Act
|
||||
connection.PortName = "SerialPort";
|
||||
connection.BaudRate = BaudRate.Baud2400;
|
||||
connection.DataBits = 5;
|
||||
connection.Handshake = Handshake.XOnXOff;
|
||||
connection.Parity = Parity.None;
|
||||
connection.ReadTimeout = TimeSpan.FromSeconds(123);
|
||||
connection.RtsEnable = true;
|
||||
connection.StopBits = StopBits.OnePointFive;
|
||||
connection.WriteTimeout = TimeSpan.FromSeconds(456);
|
||||
|
||||
// Assert - part 1
|
||||
_serialPortMock.VerifySet(p => p.PortName = "SerialPort", Times.Once);
|
||||
_serialPortMock.VerifySet(p => p.BaudRate = 2400, Times.Once);
|
||||
_serialPortMock.VerifySet(p => p.DataBits = 5, Times.Once);
|
||||
_serialPortMock.VerifySet(p => p.Handshake = Handshake.XOnXOff, Times.Once);
|
||||
_serialPortMock.VerifySet(p => p.Parity = Parity.None, Times.Once);
|
||||
_serialPortMock.VerifySet(p => p.ReadTimeout = 123000, Times.Once);
|
||||
_serialPortMock.VerifySet(p => p.RtsEnable = true, Times.Once);
|
||||
_serialPortMock.VerifySet(p => p.StopBits = StopBits.OnePointFive, Times.Once);
|
||||
_serialPortMock.VerifySet(p => p.WriteTimeout = 456000, Times.Once);
|
||||
|
||||
_serialPortMock.VerifyNoOtherCalls();
|
||||
|
||||
// Assert - part 2
|
||||
Assert.AreEqual("Serial", connection.Name);
|
||||
Assert.IsNull(connection.PortName);
|
||||
Assert.AreEqual(0, (int)connection.BaudRate);
|
||||
Assert.AreEqual(0, connection.DataBits);
|
||||
Assert.AreEqual(0, (int)connection.Handshake);
|
||||
Assert.AreEqual(0, (int)connection.Parity);
|
||||
Assert.AreEqual(1, connection.ReadTimeout.TotalSeconds);
|
||||
Assert.IsFalse(connection.RtsEnable);
|
||||
Assert.AreEqual(0, (int)connection.StopBits);
|
||||
Assert.AreEqual(1, connection.WriteTimeout.TotalSeconds);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldBeAbleToDisposeMultipleTimes()
|
||||
{
|
||||
// Arrange
|
||||
var connection = GetConnection();
|
||||
|
||||
// Act
|
||||
connection.Dispose();
|
||||
connection.Dispose();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ObjectDisposedException))]
|
||||
public async Task ShouldThrowDisposedExceptionOnInvokeAsync()
|
||||
{
|
||||
// Arrange
|
||||
var connection = GetConnection();
|
||||
connection.Dispose();
|
||||
|
||||
// Act
|
||||
await connection.InvokeAsync(null, null);
|
||||
|
||||
// Assert - OjbectDisposedException
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(null)]
|
||||
[DataRow(new byte[0])]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public async Task ShouldThrowArgumentNullExceptionForMissingRequestOnInvokeAsync(byte[] request)
|
||||
{
|
||||
// Arrange
|
||||
var connection = GetConnection();
|
||||
|
||||
// Act
|
||||
await connection.InvokeAsync(request, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public async Task ShouldThrowArgumentNullExceptionForMissingValidationOnInvokeAsync()
|
||||
{
|
||||
// Arrange
|
||||
byte[] request = new byte[1];
|
||||
var connection = GetConnection();
|
||||
|
||||
// Act
|
||||
await connection.InvokeAsync(request, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldInvokeAsync()
|
||||
{
|
||||
// Arrange
|
||||
byte[] request = [1, 2, 3];
|
||||
byte[] expectedResponse = [9, 8, 7];
|
||||
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||
_serialLineResponseQueue.Enqueue(expectedResponse);
|
||||
|
||||
var connection = GetConnection();
|
||||
|
||||
// Act
|
||||
var response = await connection.InvokeAsync(request, validation);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(response);
|
||||
|
||||
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
||||
CollectionAssert.AreEqual(request, _serialLineRequestCallbacks.First());
|
||||
|
||||
_serialPortMock.Verify(c => c.IsOpen, Times.Once);
|
||||
|
||||
_serialPortMock.Verify(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_serialPortMock.Verify(ns => ns.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
_serialPortMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(false)]
|
||||
[DataRow(true)]
|
||||
public async Task ShouldOpenAndCloseOnInvokeAsync(bool modifyDriver)
|
||||
{
|
||||
// Arrange
|
||||
_alwaysOpen = false;
|
||||
_isOpenQueue.Enqueue(false);
|
||||
_isOpenQueue.Enqueue(true);
|
||||
_isOpenQueue.Enqueue(true);
|
||||
|
||||
byte[] request = [1, 2, 3];
|
||||
byte[] expectedResponse = [9, 8, 7];
|
||||
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||
_serialLineResponseQueue.Enqueue(expectedResponse);
|
||||
|
||||
var connection = GetSerialConnection();
|
||||
connection.IdleTimeout = TimeSpan.FromMilliseconds(200);
|
||||
connection.DriverEnabledRS485 = modifyDriver;
|
||||
|
||||
// Act
|
||||
var response = await connection.InvokeAsync(request, validation);
|
||||
await Task.Delay(500);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(response);
|
||||
|
||||
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
||||
CollectionAssert.AreEqual(request, _serialLineRequestCallbacks.First());
|
||||
|
||||
_serialPortMock.VerifyGet(c => c.ReadTimeout, Times.Once);
|
||||
|
||||
_serialPortMock.Verify(c => c.IsOpen, Times.Exactly(3));
|
||||
_serialPortMock.Verify(c => c.Close(), Times.Exactly(2));
|
||||
_serialPortMock.Verify(c => c.ResetRS485DriverStateFlags(), Times.Exactly(2));
|
||||
_serialPortMock.Verify(c => c.Open(), Times.Once);
|
||||
|
||||
if (modifyDriver)
|
||||
{
|
||||
_serialPortMock.Verify(c => c.GetRS485DriverStateFlags(), Times.Once);
|
||||
_serialPortMock.Verify(c => c.ChangeRS485DriverStateFlags(It.IsAny<RS485Flags>()), Times.Once);
|
||||
}
|
||||
|
||||
_serialPortMock.Verify(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_serialPortMock.Verify(ns => ns.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
_serialPortMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(EndOfStreamException))]
|
||||
public async Task ShouldThrowEndOfStreamExceptionOnInvokeAsync()
|
||||
{
|
||||
// Arrange
|
||||
byte[] request = [1, 2, 3];
|
||||
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||
|
||||
var connection = GetConnection();
|
||||
|
||||
// Act
|
||||
var response = await connection.InvokeAsync(request, validation);
|
||||
|
||||
// Assert - EndOfStreamException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldSkipCloseOnTimeoutOnInvokeAsync()
|
||||
{
|
||||
// Arrange
|
||||
_alwaysOpen = false;
|
||||
_isOpenQueue.Enqueue(false);
|
||||
_isOpenQueue.Enqueue(true);
|
||||
_isOpenQueue.Enqueue(false);
|
||||
|
||||
byte[] request = [1, 2, 3];
|
||||
byte[] expectedResponse = [9, 8, 7];
|
||||
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||
_serialLineResponseQueue.Enqueue(expectedResponse);
|
||||
|
||||
var connection = GetConnection();
|
||||
connection.IdleTimeout = TimeSpan.FromMilliseconds(200);
|
||||
|
||||
// Act
|
||||
var response = await connection.InvokeAsync(request, validation);
|
||||
await Task.Delay(500);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(response);
|
||||
|
||||
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
||||
CollectionAssert.AreEqual(request, _serialLineRequestCallbacks.First());
|
||||
|
||||
_serialPortMock.VerifyGet(c => c.ReadTimeout, Times.Once);
|
||||
|
||||
_serialPortMock.Verify(c => c.IsOpen, Times.Exactly(3));
|
||||
_serialPortMock.Verify(c => c.Close(), Times.Once);
|
||||
_serialPortMock.Verify(c => c.ResetRS485DriverStateFlags(), Times.Once);
|
||||
_serialPortMock.Verify(c => c.Open(), Times.Once);
|
||||
|
||||
_serialPortMock.Verify(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_serialPortMock.Verify(ns => ns.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
_serialPortMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldRetryToConnectOnInvokeAsync()
|
||||
{
|
||||
// Arrange
|
||||
_alwaysOpen = false;
|
||||
_isOpenQueue.Enqueue(false);
|
||||
_isOpenQueue.Enqueue(false);
|
||||
_isOpenQueue.Enqueue(true);
|
||||
|
||||
byte[] request = [1, 2, 3];
|
||||
byte[] expectedResponse = [9, 8, 7];
|
||||
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||
_serialLineResponseQueue.Enqueue(expectedResponse);
|
||||
|
||||
var connection = GetConnection();
|
||||
|
||||
// Act
|
||||
var response = await connection.InvokeAsync(request, validation);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(response);
|
||||
|
||||
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
||||
CollectionAssert.AreEqual(request, _serialLineRequestCallbacks.First());
|
||||
|
||||
_serialPortMock.VerifyGet(c => c.ReadTimeout, Times.Exactly(2));
|
||||
|
||||
_serialPortMock.Verify(c => c.IsOpen, Times.Exactly(3));
|
||||
_serialPortMock.Verify(c => c.Close(), Times.Exactly(2));
|
||||
_serialPortMock.Verify(c => c.ResetRS485DriverStateFlags(), Times.Exactly(2));
|
||||
_serialPortMock.Verify(c => c.Open(), Times.Exactly(2));
|
||||
|
||||
_serialPortMock.Verify(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_serialPortMock.Verify(ns => ns.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
_serialPortMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(TaskCanceledException))]
|
||||
public async Task ShouldThrowTaskCancelledExceptionForDisposeOnInvokeAsync()
|
||||
{
|
||||
// Arrange
|
||||
byte[] request = [1, 2, 3];
|
||||
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||
|
||||
var connection = GetConnection();
|
||||
_serialPortMock
|
||||
.Setup(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.Delay(100));
|
||||
|
||||
// Act
|
||||
var task = connection.InvokeAsync(request, validation);
|
||||
connection.Dispose();
|
||||
await task;
|
||||
|
||||
// Assert - TaskCancelledException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(TaskCanceledException))]
|
||||
public async Task ShouldThrowTaskCancelledExceptionForCancelOnInvokeAsync()
|
||||
{
|
||||
// Arrange
|
||||
byte[] request = [1, 2, 3];
|
||||
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
var connection = GetConnection();
|
||||
_serialPortMock
|
||||
.Setup(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.Delay(100));
|
||||
|
||||
// Act
|
||||
var task = connection.InvokeAsync(request, validation, cts.Token);
|
||||
cts.Cancel();
|
||||
await task;
|
||||
|
||||
// Assert - TaskCancelledException
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldRemoveRequestFromQueueOnInvokeAsync()
|
||||
{
|
||||
// Arrange
|
||||
byte[] request = [1, 2, 3];
|
||||
byte[] expectedResponse = [9, 8, 7];
|
||||
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||
_serialLineResponseQueue.Enqueue(expectedResponse);
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
var connection = GetConnection();
|
||||
_serialPortMock
|
||||
.Setup(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<byte[], CancellationToken>((req, _) => _serialLineRequestCallbacks.Add(req.ToArray()))
|
||||
.Returns(Task.Delay(100));
|
||||
|
||||
// Act
|
||||
var taskToComplete = connection.InvokeAsync(request, validation);
|
||||
|
||||
var taskToCancel = connection.InvokeAsync(request, validation, cts.Token);
|
||||
cts.Cancel();
|
||||
|
||||
var response = await taskToComplete;
|
||||
|
||||
// Assert - Part 1
|
||||
try
|
||||
{
|
||||
await taskToCancel;
|
||||
Assert.Fail();
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{ /* expected exception */ }
|
||||
|
||||
// Assert - Part 2
|
||||
Assert.AreEqual(1, _serialLineRequestCallbacks.Count);
|
||||
CollectionAssert.AreEqual(request, _serialLineRequestCallbacks.First());
|
||||
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
||||
|
||||
_serialPortMock.Verify(c => c.IsOpen, Times.Once);
|
||||
|
||||
_serialPortMock.Verify(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_serialPortMock.Verify(ns => ns.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
_serialPortMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldRemoveRequestFromQueueOnDispose()
|
||||
{
|
||||
// Arrange
|
||||
byte[] request = [1, 2, 3];
|
||||
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||
|
||||
var connection = GetConnection();
|
||||
_serialPortMock
|
||||
.Setup(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<byte[], CancellationToken>((req, _) => _serialLineRequestCallbacks.Add(req.ToArray()))
|
||||
.Returns(Task.Delay(100));
|
||||
|
||||
// Act
|
||||
var taskToCancel = connection.InvokeAsync(request, validation);
|
||||
var taskToDequeue = connection.InvokeAsync(request, validation);
|
||||
connection.Dispose();
|
||||
|
||||
// Assert
|
||||
try
|
||||
{
|
||||
await taskToCancel;
|
||||
Assert.Fail();
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{ /* expected exception */ }
|
||||
|
||||
try
|
||||
{
|
||||
await taskToDequeue;
|
||||
Assert.Fail();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{ /* expected exception */ }
|
||||
|
||||
Assert.AreEqual(1, _serialLineRequestCallbacks.Count);
|
||||
CollectionAssert.AreEqual(request, _serialLineRequestCallbacks.First());
|
||||
|
||||
_serialPortMock.Verify(c => c.IsOpen, Times.Once);
|
||||
_serialPortMock.Verify(c => c.Dispose(), Times.Once);
|
||||
|
||||
_serialPortMock.Verify(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
_serialPortMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
private IModbusConnection GetConnection()
|
||||
=> GetSerialConnection();
|
||||
|
||||
private ModbusSerialConnection GetSerialConnection()
|
||||
{
|
||||
_serialPortMock = new Mock<SerialPortWrapper>();
|
||||
_serialPortMock.Setup(p => p.IsOpen).Returns(() => _alwaysOpen || _isOpenQueue.Dequeue());
|
||||
_serialPortMock.Setup(p => p.ReadTimeout).Returns(() => _serialPortReadTimeout);
|
||||
_serialPortMock.Setup(p => p.WriteTimeout).Returns(() => _serialPortWriteTimeout);
|
||||
|
||||
_serialPortMock
|
||||
.Setup(p => p.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<byte[], CancellationToken>((req, _) => _serialLineRequestCallbacks.Add(req))
|
||||
.Returns(Task.CompletedTask);
|
||||
_serialPortMock
|
||||
.Setup(p => p.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||
.Returns<byte[], int, int, CancellationToken>((buffer, offset, count, _) =>
|
||||
{
|
||||
if (_serialLineResponseQueue.TryDequeue(out byte[] bytes))
|
||||
{
|
||||
int len = bytes.Length < count ? bytes.Length : count;
|
||||
Array.Copy(bytes, 0, buffer, offset, len);
|
||||
return Task.FromResult(len);
|
||||
}
|
||||
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
|
||||
var connection = new ModbusSerialConnection();
|
||||
|
||||
// Replace real connection with mock
|
||||
var connectionField = connection.GetType().GetField("_serialPort", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
(connectionField.GetValue(connection) as SerialPortWrapper)?.Dispose();
|
||||
connectionField.SetValue(connection, _serialPortMock.Object);
|
||||
|
||||
// Set unit test mode
|
||||
connection.GetType().GetField("_isUnitTest", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(connection, true);
|
||||
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,8 @@ It uses a specific TCP connection implementation and plugs all things from the C
|
||||
---
|
||||
|
||||
Published under [MIT License] (see [**tl;dr**Legal])
|
||||
[](https://codeium.com/profile/andreasmueller)
|
||||
[](https://link.am-wd.de/donate)
|
||||
[](https://link.am-wd.de/codeium)
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user