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