Implemented Modbus Serial Client
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user