diff --git a/src/Api/Readers/TWN4ReaderDevice/TWN4ReaderDevice.Core.cs b/src/Api/Readers/TWN4ReaderDevice/TWN4ReaderDevice.Core.cs index 3a0b4f4..9c06937 100644 --- a/src/Api/Readers/TWN4ReaderDevice/TWN4ReaderDevice.Core.cs +++ b/src/Api/Readers/TWN4ReaderDevice/TWN4ReaderDevice.Core.cs @@ -42,6 +42,8 @@ public partial class TWN4ReaderDevice : IDisposable private static readonly object syncRoot = new object(); private static List instance; + // Serialize access to the underlying transport to prevent interleaved async commands. + private readonly SemaphoreSlim _transportLock = new SemaphoreSlim(1, 1); private readonly Func _transportFactory; private IReaderTransport _transport; @@ -811,6 +813,7 @@ public async Task CallFunctionAsync(byte[] CMD) /// private async Task DoTXRXAsync(byte[] CMD) { + await _transportLock.WaitAsync().ConfigureAwait(false); try { EnsureTransport(); @@ -836,6 +839,10 @@ private async Task DoTXRXAsync(byte[] CMD) { throw new ReaderException("Call was not successfull, error " + Enum.GetName(typeof(ReaderError), ReaderError.NotOpen), null); } + finally + { + _transportLock.Release(); + } }// End of DoTXRXAsync diff --git a/tests/Elatec.NET.Tests/ConcurrentReaderTransport.cs b/tests/Elatec.NET.Tests/ConcurrentReaderTransport.cs new file mode 100644 index 0000000..32836da --- /dev/null +++ b/tests/Elatec.NET.Tests/ConcurrentReaderTransport.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Elatec.NET.Interfaces; + +namespace Elatec.NET.Tests +{ + /// + /// Transport stub that detects overlapping Write/Read cycles to ensure calls are serialized. + /// + public class ConcurrentReaderTransport : IReaderTransport + { + private int _inFlight; + + public ConcurrentReaderTransport(string portName) + { + PortName = portName; + } + + public string PortName { get; } + + public int ReadTimeout { get; set; } + + public int WriteTimeout { get; set; } + + public bool IsOpen { get; private set; } + + public bool OverlapDetected { get; private set; } + + public Queue Responses { get; } = new Queue(); + + public List WrittenLines { get; } = new List(); + + public TaskCompletionSource FirstWriteSeen { get; } = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + public TaskCompletionSource AllowRead { get; } = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + public event EventHandler ErrorReceived; + + public Task ConnectAsync() + { + IsOpen = true; + return Task.CompletedTask; + } + + public Task DisconnectAsync() + { + IsOpen = false; + return Task.CompletedTask; + } + + public void DiscardInBuffer() + { + } + + public void DiscardOutBuffer() + { + } + + public async Task ReadLineAsync() + { + await AllowRead.Task.ConfigureAwait(false); + Interlocked.Exchange(ref _inFlight, 0); + + if (Responses.Count == 0) + { + throw new InvalidOperationException("No responses configured."); + } + + return Responses.Dequeue(); + } + + public Task WriteLineAsync(string data) + { + if (Interlocked.CompareExchange(ref _inFlight, 1, 0) == 1) + { + OverlapDetected = true; + } + + WrittenLines.Add(data); + FirstWriteSeen.TrySetResult(true); + return Task.CompletedTask; + } + + public void Dispose() + { + IsOpen = false; + } + } +} diff --git a/tests/Elatec.NET.Tests/TWN4ReaderDeviceTests.cs b/tests/Elatec.NET.Tests/TWN4ReaderDeviceTests.cs index 06d060d..40f8b0a 100644 --- a/tests/Elatec.NET.Tests/TWN4ReaderDeviceTests.cs +++ b/tests/Elatec.NET.Tests/TWN4ReaderDeviceTests.cs @@ -102,5 +102,24 @@ public async Task SleepAsync_UsesSysApi() Assert.Equal(0x01, result); Assert.Single(transport.WrittenLines, "00070500000001000000"); } + + [Fact] + public async Task CallFunctionRawAsync_SerializesConcurrentCalls() + { + var transport = new ConcurrentReaderTransport("COM8"); + transport.Responses.Enqueue("00"); + transport.Responses.Enqueue("00"); + var device = new TWN4ReaderDevice("COM8", _ => transport); + + var firstTask = device.CallFunctionRawAsync(new byte[] { 0xAA }); + await transport.FirstWriteSeen.Task; + var secondTask = device.CallFunctionRawAsync(new byte[] { 0xBB }); + + transport.AllowRead.TrySetResult(true); + await Task.WhenAll(firstTask, secondTask); + + Assert.False(transport.OverlapDetected); + Assert.Equal(2, transport.WrittenLines.Count); + } } }