Implemented Modbus Serial Client

This commit is contained in:
2024-04-02 19:24:36 +02:00
parent ca95298390
commit a458e76aea
18 changed files with 1975 additions and 17 deletions

View File

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

View 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
}
}

View 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
}
}

View 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}";
}
}
}
}

View 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;
}
}
}
}

View 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
}
}

View 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;
}
}

View 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
}
}

View 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;
}
}

View 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);
}
}