diff --git a/src/Drivers/Transport/SerialPortAdapter.cs b/src/Drivers/Transport/SerialPortAdapter.cs new file mode 100644 index 0000000..7491c86 --- /dev/null +++ b/src/Drivers/Transport/SerialPortAdapter.cs @@ -0,0 +1,152 @@ +using System; +using System.IO.Ports; + +namespace Elatec.NET.System +{ + /// + /// Provides an abstraction layer around for testing. + /// + internal interface ISerialPortAdapter : IDisposable + { + string PortName { get; set; } + + int BaudRate { get; set; } + + int DataBits { get; set; } + + StopBits StopBits { get; set; } + + Parity Parity { get; set; } + + string NewLine { get; set; } + + int ReadTimeout { get; set; } + + int WriteTimeout { get; set; } + + bool IsOpen { get; } + + event SerialErrorReceivedEventHandler ErrorReceived; + + void Open(); + + void Close(); + + void DiscardInBuffer(); + + void DiscardOutBuffer(); + + string ReadLine(); + + void WriteLine(string data); + } + + internal sealed class SerialPortAdapter : ISerialPortAdapter + { + private readonly SerialPort _serialPort; + + public SerialPortAdapter(string portName) + { + _serialPort = new SerialPort + { + PortName = portName, + BaudRate = 9600, + DataBits = 8, + StopBits = StopBits.One, + Parity = Parity.None, + NewLine = "\r" + }; + } + + public string PortName + { + get => _serialPort.PortName; + set => _serialPort.PortName = value; + } + + public int BaudRate + { + get => _serialPort.BaudRate; + set => _serialPort.BaudRate = value; + } + + public int DataBits + { + get => _serialPort.DataBits; + set => _serialPort.DataBits = value; + } + + public StopBits StopBits + { + get => _serialPort.StopBits; + set => _serialPort.StopBits = value; + } + + public Parity Parity + { + get => _serialPort.Parity; + set => _serialPort.Parity = value; + } + + public string NewLine + { + get => _serialPort.NewLine; + set => _serialPort.NewLine = value; + } + + public int ReadTimeout + { + get => _serialPort.ReadTimeout; + set => _serialPort.ReadTimeout = value; + } + + public int WriteTimeout + { + get => _serialPort.WriteTimeout; + set => _serialPort.WriteTimeout = value; + } + + public bool IsOpen => _serialPort.IsOpen; + + public event SerialErrorReceivedEventHandler ErrorReceived + { + add => _serialPort.ErrorReceived += value; + remove => _serialPort.ErrorReceived -= value; + } + + public void Open() + { + _serialPort.Open(); + } + + public void Close() + { + _serialPort.Close(); + } + + public void DiscardInBuffer() + { + _serialPort.DiscardInBuffer(); + } + + public void DiscardOutBuffer() + { + _serialPort.DiscardOutBuffer(); + } + + public string ReadLine() + { + return _serialPort.ReadLine(); + } + + public void WriteLine(string data) + { + _serialPort.WriteLine(data); + } + + public void Dispose() + { + _serialPort.Dispose(); + } + } +} diff --git a/src/Drivers/Transport/SerialPortTransport.cs b/src/Drivers/Transport/SerialPortTransport.cs index fe244a3..8b52803 100644 --- a/src/Drivers/Transport/SerialPortTransport.cs +++ b/src/Drivers/Transport/SerialPortTransport.cs @@ -2,6 +2,7 @@ using System.IO; using System.IO.Ports; using System.Threading.Tasks; +using System.Threading; using Elatec.NET.Interfaces; namespace Elatec.NET.System @@ -11,29 +12,27 @@ namespace Elatec.NET.System /// public class SerialPortTransport : IReaderTransport { - private readonly SerialPort _serialPort; + private readonly ISerialPortAdapter _serialPort; + private readonly SemaphoreSlim _connectionLock = new SemaphoreSlim(1, 1); + private bool _disposed; /// /// Initializes a new instance of the class for the given port. /// /// Name of the serial port used to reach the reader. public SerialPortTransport(string portName) + : this(CreateConfiguredPort(portName)) { - if (string.IsNullOrWhiteSpace(portName)) - { - throw new ArgumentException("Port name must be provided.", nameof(portName)); - } + } - _serialPort = new SerialPort + internal SerialPortTransport(ISerialPortAdapter serialPort) + { + if (serialPort == null) { - PortName = portName, - BaudRate = 9600, - DataBits = 8, - StopBits = StopBits.One, - Parity = Parity.None, - NewLine = "\r" - }; + throw new ArgumentNullException(nameof(serialPort)); + } + _serialPort = serialPort; _serialPort.ErrorReceived += OnErrorReceived; } @@ -63,19 +62,30 @@ public int WriteTimeout /// public async Task ConnectAsync() { - await Task.Run(() => + ThrowIfDisposed(); + + await _connectionLock.WaitAsync().ConfigureAwait(false); + try { + ThrowIfDisposed(); if (!_serialPort.IsOpen) { _serialPort.Open(); } - }).ConfigureAwait(false); + } + finally + { + _connectionLock.Release(); + } } /// public async Task DisconnectAsync() { - await Task.Run(() => + ThrowIfDisposed(); + + await _connectionLock.WaitAsync().ConfigureAwait(false); + try { if (_serialPort.IsOpen) { @@ -83,49 +93,97 @@ await Task.Run(() => _serialPort.DiscardOutBuffer(); _serialPort.Close(); } - }).ConfigureAwait(false); + } + finally + { + _connectionLock.Release(); + } } /// public void DiscardInBuffer() { + ThrowIfDisposed(); _serialPort.DiscardInBuffer(); } /// public void DiscardOutBuffer() { + ThrowIfDisposed(); _serialPort.DiscardOutBuffer(); } /// public async Task ReadLineAsync() { + ThrowIfDisposed(); return await Task.Run(() => _serialPort.ReadLine()).ConfigureAwait(false); } /// public async Task WriteLineAsync(string data) { + ThrowIfDisposed(); await Task.Run(() => _serialPort.WriteLine(data)).ConfigureAwait(false); } /// public void Dispose() { - _serialPort.ErrorReceived -= OnErrorReceived; - - if (_serialPort.IsOpen) + if (_disposed) { - _serialPort.Close(); + return; } - _serialPort.Dispose(); + _connectionLock.Wait(); + try + { + if (_disposed) + { + return; + } + + _disposed = true; + _serialPort.ErrorReceived -= OnErrorReceived; + + if (_serialPort.IsOpen) + { + _serialPort.DiscardInBuffer(); + _serialPort.DiscardOutBuffer(); + _serialPort.Close(); + } + + _serialPort.Dispose(); + } + finally + { + _connectionLock.Release(); + _connectionLock.Dispose(); + } } private void OnErrorReceived(object sender, SerialErrorReceivedEventArgs e) { ErrorReceived?.Invoke(this, new IOException($"Serial port error: {e.EventType}")); } + + private static ISerialPortAdapter CreateConfiguredPort(string portName) + { + if (string.IsNullOrWhiteSpace(portName)) + { + throw new ArgumentException("Port name must be provided.", nameof(portName)); + } + + return new SerialPortAdapter(portName); + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(SerialPortTransport)); + } + } } } diff --git a/tests/Elatec.NET.Tests/SerialPortTransportTests.cs b/tests/Elatec.NET.Tests/SerialPortTransportTests.cs new file mode 100644 index 0000000..0369d5d --- /dev/null +++ b/tests/Elatec.NET.Tests/SerialPortTransportTests.cs @@ -0,0 +1,134 @@ +using System; +using System.IO.Ports; +using System.Threading.Tasks; +using Elatec.NET.System; +using Xunit; + +namespace Elatec.NET.Tests +{ + public class SerialPortTransportTests + { + [Fact] + public async Task ConnectAsync_OpensPortOnce() + { + var adapter = new FakeSerialPortAdapter(); + using var transport = new SerialPortTransport(adapter); + + await transport.ConnectAsync(); + await transport.ConnectAsync(); + + Assert.True(adapter.IsOpen); + Assert.Equal(1, adapter.OpenCallCount); + } + + [Fact] + public async Task DisconnectAsync_ClosesAndDiscardsBuffers() + { + var adapter = new FakeSerialPortAdapter(); + using var transport = new SerialPortTransport(adapter); + + await transport.ConnectAsync(); + await transport.DisconnectAsync(); + + Assert.False(adapter.IsOpen); + Assert.Equal(1, adapter.DiscardInBufferCallCount); + Assert.Equal(1, adapter.DiscardOutBufferCallCount); + Assert.Equal(1, adapter.CloseCallCount); + } + + [Fact] + public async Task Dispose_ClosesOpenPort() + { + var adapter = new FakeSerialPortAdapter(); + var transport = new SerialPortTransport(adapter); + + await transport.ConnectAsync(); + transport.Dispose(); + + Assert.False(adapter.IsOpen); + Assert.Equal(1, adapter.DiscardInBufferCallCount); + Assert.Equal(1, adapter.DiscardOutBufferCallCount); + Assert.Equal(1, adapter.CloseCallCount); + } + + [Fact] + public async Task ConnectAsync_ThrowsWhenDisposed() + { + var adapter = new FakeSerialPortAdapter(); + var transport = new SerialPortTransport(adapter); + + transport.Dispose(); + + await Assert.ThrowsAsync(() => transport.ConnectAsync()); + } + + private sealed class FakeSerialPortAdapter : ISerialPortAdapter + { + public string PortName { get; set; } = "COM1"; + + public int BaudRate { get; set; } + + public int DataBits { get; set; } + + public StopBits StopBits { get; set; } + + public Parity Parity { get; set; } + + public string NewLine { get; set; } = "\r"; + + public int ReadTimeout { get; set; } + + public int WriteTimeout { get; set; } + + public bool IsOpen { get; private set; } + + public int OpenCallCount { get; private set; } + + public int CloseCallCount { get; private set; } + + public int DiscardInBufferCallCount { get; private set; } + + public int DiscardOutBufferCallCount { get; private set; } + +#pragma warning disable CS0067 + public event SerialErrorReceivedEventHandler ErrorReceived; +#pragma warning restore CS0067 + + public void Open() + { + OpenCallCount++; + IsOpen = true; + } + + public void Close() + { + CloseCallCount++; + IsOpen = false; + } + + public void DiscardInBuffer() + { + DiscardInBufferCallCount++; + } + + public void DiscardOutBuffer() + { + DiscardOutBufferCallCount++; + } + + public string ReadLine() + { + return "OK"; + } + + public void WriteLine(string data) + { + } + + public void Dispose() + { + IsOpen = false; + } + } + } +}