using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Text; using System.Threading; using Windows.Win32; using Windows.Win32.Devices.Communication; using Windows.Win32.Foundation; using Windows.Win32.Storage.FileSystem; using Microsoft.Win32.SafeHandles; namespace Nefarius.Peripherals.SerialPort; /// /// Wrapper class around a serial (COM, RS-232) port. /// [SuppressMessage("ReSharper", "UnusedMember.Global")] public partial class SerialPort : IDisposable { private readonly ManualResetEvent _writeEvent = new(false); private bool _auto; private bool _checkSends = true; private CancellationTokenSource _cts; private Handshake _handShake; private SafeFileHandle _hPort; private bool _online; private NativeOverlapped _ptrUwo; private Exception _rxException; private bool _rxExceptionReported; private Thread _rxThread; private int _stateBrk = 2; private int _stateDtr = 2; private int _stateRts = 2; private int _writeCount; /// /// Class constructor /// public SerialPort(string portName) { PortName = portName; } /// public SerialPort(string portName, int baudRate) : this(portName) { BaudRate = baudRate; } /// public void Dispose() { Close(); } /// /// Opens the com port and configures it with the required settings. /// /// false if the port could not be opened due to missing permissions. public bool Open() { DCB portDcb = new(); COMMTIMEOUTS commTimeouts = new(); if (_online) { return false; } _hPort = PInvoke.CreateFile(PortName, (uint)(FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE), 0, null, FILE_CREATION_DISPOSITION.OPEN_EXISTING, FILE_FLAGS_AND_ATTRIBUTES.FILE_FLAG_OVERLAPPED, null ); if (_hPort.IsInvalid) { if (Marshal.GetLastWin32Error() == (int)WIN32_ERROR.ERROR_ACCESS_DENIED) { return false; } throw new CommPortException("Port Open Failure", Marshal.GetLastWin32Error()); } _online = true; commTimeouts.ReadIntervalTimeout = 0; commTimeouts.ReadTotalTimeoutConstant = 0; commTimeouts.ReadTotalTimeoutMultiplier = 0; commTimeouts.WriteTotalTimeoutConstant = (uint)SendTimeoutConstant; commTimeouts.WriteTotalTimeoutMultiplier = (uint)SendTimeoutMultiplier; portDcb.Init(Parity is Parity.Odd or Parity.Even, TxFlowCts, TxFlowDsr, (int)UseDtr, RxGateDsr, !TxWhenRxXoff, TxFlowX, RxFlowX, (int)UseRts); portDcb.BaudRate = (uint)BaudRate; portDcb.ByteSize = (byte)DataBits; portDcb.Parity = (DCB_PARITY)Parity; portDcb.StopBits = (DCB_STOP_BITS)StopBits; portDcb.XoffChar = new CHAR((sbyte)XoffChar); portDcb.XonChar = new CHAR((sbyte)XonChar); portDcb.XoffLim = (ushort)RxHighWater; portDcb.XonLim = (ushort)RxLowWater; if (RxQueue != 0 || TxQueue != 0) { if (!PInvoke.SetupComm(_hPort, (uint)RxQueue, (uint)TxQueue)) { ThrowException("Bad queue settings"); } } if (!PInvoke.SetCommState(_hPort, portDcb)) { ThrowException("Bad com settings"); } if (!PInvoke.SetCommTimeouts(_hPort, commTimeouts)) { ThrowException("Bad timeout settings"); } _stateBrk = 0; switch (UseDtr) { case HsOutput.None: _stateDtr = 0; break; case HsOutput.Online: _stateDtr = 1; break; } switch (UseRts) { case HsOutput.None: _stateRts = 0; break; case HsOutput.Online: _stateRts = 1; break; } _checkSends = CheckAllSends; _ptrUwo.EventHandle = _checkSends ? _writeEvent.SafeWaitHandle.DangerousGetHandle() : IntPtr.Zero; _writeCount = 0; _rxException = null; _rxExceptionReported = false; _cts = new CancellationTokenSource(); _rxThread = new Thread(ReceiveThread) { Name = "CommBaseRx", Priority = ThreadPriority.AboveNormal, IsBackground = true }; _rxThread.Start(); Thread.Sleep(1); //Give rx thread time to start. By documentation, 0 should work, but it does not! _auto = false; if (AfterOpen()) { _auto = AutoReopen; return true; } Close(); return false; } /// /// Closes the com port. /// public void Close() { if (!_online) { return; } _auto = false; BeforeClose(false); InternalClose(); _rxException = null; } private void InternalClose() { PInvoke.CancelIo(_hPort); if (_rxThread != null) { _cts.Cancel(); _rxThread.Join(); _rxThread = null; } _hPort.Dispose(); _stateRts = 2; _stateDtr = 2; _stateBrk = 2; _online = false; } /// /// Destructor (just in case) /// ~SerialPort() { Close(); } /// /// Block until all bytes in the queue have been transmitted. /// public void Flush() { CheckOnline(); CheckResult(); } /// /// Use this to throw exceptions in derived classes. Correctly handles threading issues /// and closes the port if necessary. /// /// Description of fault protected void ThrowException(string reason) { if (Thread.CurrentThread == _rxThread) { throw new CommPortException(reason); } if (_online) { BeforeClose(true); InternalClose(); } if (_rxException == null) { throw new CommPortException(reason); } throw new CommPortException(_rxException); } /// /// Queues bytes for transmission. /// /// Array of bytes to be sent public unsafe void Write(byte[] toSend) { CheckOnline(); CheckResult(); _writeCount = toSend.GetLength(0); fixed (NativeOverlapped* ptrOl = &_ptrUwo) { uint sent; if (PInvoke.WriteFile(_hPort, toSend.AsSpan(), &sent, ptrOl)) { _writeCount -= (int)sent; } else { if (Marshal.GetLastWin32Error() != (int)WIN32_ERROR.ERROR_IO_PENDING) { ThrowException("Unexpected failure"); } } } } /// /// Queues string for transmission. /// /// Array of bytes to be sent public void Write(string toSend) { Write(new ASCIIEncoding().GetBytes(toSend)); } /// /// Queues a single byte for transmission. /// /// Byte to be sent public void Write(byte toSend) { byte[] b = new byte[1]; b[0] = toSend; Write(b); } /// /// Queues a single char for transmission. /// /// Byte to be sent public void Write(char toSend) { Write(toSend.ToString()); } /// /// Queues string with a new line ("\r\n") for transmission. /// /// Array of bytes to be sent public void WriteLine(string toSend) { Write(new ASCIIEncoding().GetBytes(toSend + Environment.NewLine)); } private void CheckResult() { if (_writeCount <= 0) { return; } if (PInvoke.GetOverlappedResult(_hPort, _ptrUwo, out uint sent, _checkSends)) { _writeCount -= (int)sent; if (_writeCount != 0) { ThrowException("Send Timeout"); } } else { if (Marshal.GetLastWin32Error() != (int)WIN32_ERROR.ERROR_IO_PENDING) { ThrowException("Unexpected failure"); } } } /// /// Sends a protocol byte immediately ahead of any queued bytes. /// /// Byte to send /// False if an immediate byte is already scheduled and not yet sent public void SendImmediate(byte tosend) { CheckOnline(); if (!PInvoke.TransmitCommChar(_hPort, new CHAR((sbyte)tosend))) { ThrowException("Transmission failure"); } } /// /// Gets the status of the modem control input signals. /// /// Modem status object protected ModemStatus GetModemStatus() { CheckOnline(); if (!PInvoke.GetCommModemStatus(_hPort, out MODEM_STATUS_FLAGS f)) { ThrowException("Unexpected failure"); } return new ModemStatus(f); } /// /// Get the status of the queues /// /// Queue status object protected unsafe QueueStatus GetQueueStatus() { COMSTAT cs; COMMPROP cp = new(); CLEAR_COMM_ERROR_FLAGS er; CheckOnline(); if (!PInvoke.ClearCommError(_hPort, &er, &cs)) { ThrowException("Unexpected failure"); } if (!PInvoke.GetCommProperties(_hPort, &cp)) { ThrowException("Unexpected failure"); } return new QueueStatus(cs._bitfield, cs.cbInQue, cs.cbOutQue, cp.dwCurrentRxQueue, cp.dwCurrentTxQueue); } /// /// Override this to provide processing after the port is opened (i.e. to configure remote /// device or just check presence). /// /// false to close the port again protected virtual bool AfterOpen() { return true; } /// /// Override this to provide processing prior to port closure. /// /// True if closing due to an error protected virtual void BeforeClose(bool error) { } /// /// Invoked when a single character has been received. /// public event Action DataReceived; /// /// Override this to process received bytes. /// /// The byte that was received protected void OnRxChar(byte ch) { DataReceived?.Invoke(ch); } /// /// Override this to take action when transmission is complete (i.e. all bytes have actually /// been sent, not just queued). /// protected virtual void OnTxDone() { } /// /// Override this to take action when a break condition is detected on the input line. /// protected virtual void OnBreak() { } /// /// Override this to take action when a ring condition is signaled by an attached modem. /// protected virtual void OnRing() { } /// /// Override this to take action when one or more modem status inputs change state /// /// The status inputs that have changed state /// The state of the status inputs protected virtual void OnStatusChange(ModemStatus mask, ModemStatus state) { } /// /// Override this to take action when the reception thread closes due to an exception being thrown. /// /// The exception which was thrown protected virtual void OnRxException(Exception e) { } private unsafe void ReceiveThread() { byte[] buffer = new byte[1]; AutoResetEvent sg = new(false); NativeOverlapped ov = new() { EventHandle = sg.SafeWaitHandle.DangerousGetHandle() }; try { while (!_cts.IsCancellationRequested) { COMM_EVENT_MASK eventMask = 0; if (!PInvoke.SetCommMask(_hPort, COMM_EVENT_MASK.EV_RXCHAR | COMM_EVENT_MASK.EV_TXEMPTY | COMM_EVENT_MASK.EV_CTS | COMM_EVENT_MASK.EV_DSR | COMM_EVENT_MASK.EV_BREAK | COMM_EVENT_MASK.EV_RLSD | COMM_EVENT_MASK.EV_RING | COMM_EVENT_MASK.EV_ERR)) { throw new CommPortException("IO Error [001]"); } if (!PInvoke.WaitCommEvent(_hPort, ref eventMask, &ov)) { if (Marshal.GetLastWin32Error() == (int)WIN32_ERROR.ERROR_IO_PENDING) { WaitHandle.WaitAny(new[] { sg, _cts.Token.WaitHandle }); } else { throw new CommPortException("IO Error [002]"); } } if ((eventMask & COMM_EVENT_MASK.EV_ERR) != 0) { CLEAR_COMM_ERROR_FLAGS errs; if (PInvoke.ClearCommError(_hPort, &errs, null)) { StringBuilder s = new("UART Error: ", 40); if (((uint)errs & (uint)CLEAR_COMM_ERROR_FLAGS.CE_FRAME) != 0) { s = s.Append("Framing,"); } if (((uint)errs & PInvoke.CE_IOE) != 0) { s = s.Append("IO,"); } if (((uint)errs & (uint)CLEAR_COMM_ERROR_FLAGS.CE_OVERRUN) != 0) { s = s.Append("Overrun,"); } if (((uint)errs & (uint)CLEAR_COMM_ERROR_FLAGS.CE_RXOVER) != 0) { s = s.Append("Receive Overflow,"); } if (((uint)errs & (uint)CLEAR_COMM_ERROR_FLAGS.CE_RXPARITY) != 0) { s = s.Append("Parity,"); } if (((uint)errs & PInvoke.CE_TXFULL) != 0) { s = s.Append("Transmit Overflow,"); } s.Length -= 1; throw new CommPortException(s.ToString()); } throw new CommPortException("IO Error [003]"); } if ((eventMask & COMM_EVENT_MASK.EV_RXCHAR) != 0) { uint gotBytes; do { if (!PInvoke.ReadFile(_hPort, buffer, &gotBytes, &ov)) { if (Marshal.GetLastWin32Error() == (int)WIN32_ERROR.ERROR_IO_PENDING) { PInvoke.CancelIo(_hPort); gotBytes = 0; } else { throw new CommPortException("IO Error [004]"); } } if (gotBytes == 1) { OnRxChar(buffer[0]); } } while (gotBytes > 0); } if ((eventMask & COMM_EVENT_MASK.EV_TXEMPTY) != 0) { OnTxDone(); } if ((eventMask & COMM_EVENT_MASK.EV_BREAK) != 0) { OnBreak(); } MODEM_STATUS_FLAGS i = 0; if ((eventMask & COMM_EVENT_MASK.EV_CTS) != 0) { i |= MODEM_STATUS_FLAGS.MS_CTS_ON; } if ((eventMask & COMM_EVENT_MASK.EV_DSR) != 0) { i |= MODEM_STATUS_FLAGS.MS_DSR_ON; } if ((eventMask & COMM_EVENT_MASK.EV_RLSD) != 0) { i |= MODEM_STATUS_FLAGS.MS_RLSD_ON; } if ((eventMask & COMM_EVENT_MASK.EV_RING) != 0) { i |= MODEM_STATUS_FLAGS.MS_RING_ON; } if (i != 0) { if (!PInvoke.GetCommModemStatus(_hPort, out MODEM_STATUS_FLAGS f)) { throw new CommPortException("IO Error [005]"); } OnStatusChange(new ModemStatus(i), new ModemStatus(f)); } } } catch (Exception e) { if (e is not ThreadAbortException) { _rxException = e; OnRxException(e); } } } private bool CheckOnline() { if (_rxException != null && !_rxExceptionReported) { _rxExceptionReported = true; ThrowException("rx"); } if (_online) { if (PInvoke.GetHandleInformation(_hPort, out uint _)) { return true; } ThrowException("Offline"); return false; } if (_auto) { if (Open()) { return true; } } ThrowException("Offline"); return false; } }