0

Hi I have a simple communication on serial port well all is according to book and documentation so open port method looks like this:

    public SerialPort OpenPort(string portName)
    {
        Port = new SerialPort(portName, BaudRate);
        try
        {
            Port.Open();
            Port.DtrEnable = true;
            Port.RtsEnable = true;

            Port.DataReceived += DataReceivedEvent;
        }
        catch (Exception e)
        {
            Console.WriteLine($"ERRROR: {e.Message}");
        }

        return Port;
    }

Here we have an event on data read:

    private async void DataReceivedEvent(object sender, SerialDataReceivedEventArgs e)
    {
        var data = new byte[Port.BytesToRead];
        await Port.BaseStream.ReadAsync(data, 0, data.Length);

        Response = data;

        isFinished = true;
    }

Well all is fine and dandy, but now i want to send a message on demand and store response in a property, also i want to add cancellation token on that task timeout. So i came up with this method:

        public async Task SendMessenge(byte[] messange)
    {
        var cancellationTokenSource = new CancellationTokenSource();
        CancellationToken token = cancellationTokenSource.Token;
        cancellationTokenSource.CancelAfter(5000);
        token.ThrowIfCancellationRequested();

        isFinished = false;
        try
        {
            Task worker = Task.Run(() =>
            {
                while (!isFinished)
                {
                }
            }, token);

            await Port.BaseStream.WriteAsync(messange, 0, messange.Length, token);
            await worker;
        }
        catch (OperationCanceledException e)
        {
            throw new OperationCanceledException(e.Message, e, token);
        }
    }

Problem is with this while loop, if it is task it goes into endless loop, and it does not capture timeout token, if i put it outside a task and remove worker it works but im loosing cancellation token. I guess i could do some manual countdown like:

double WaitTimeout = Timeout + DateAndTime.Now.TimeOfDay.TotalMilliseconds;
while (!(DateAndTime.Now.TimeOfDay.TotalMilliseconds >= WaitTimeout)|| !isFalse) 

But it looks ugly.

So i think my basic question is how to effectively await for event to response and get a timeout?

Wojciech Szabowicz
  • 3,646
  • 5
  • 43
  • 87
  • `while (!isFinished) { }`<= this is terrible ... why not `ManualEventResetSlim` ... more over it has `Wait(timeout)` ... – Selvin Nov 16 '18 at 10:33
  • 1
    Exception handling is a pet peeve of mine and yours is suboptimal. For starters, you are going on after a fatal exception and do not properly log/expose stuff. here are two articles on the mater I link often: https://blogs.msdn.microsoft.com/ericlippert/2008/09/10/vexing-exceptions/ | https://www.codeproject.com/Articles/9538/Exception-Handling-Best-Practices-in-NET – Christopher Nov 16 '18 at 10:36
  • 1
    change `isFinished` from `bool` to `ManualEventResetSlim` like `ManualEventResetSlim isFinished=ManualEventResetSlim ();` instead setting it to `false/true` use `Reset/Set` then instead `await worker` use `isFinished.Wait(Timeout)` – Selvin Nov 16 '18 at 10:40
  • Do you know how many bytes to expect in response? – Leonid Vasilev Dec 04 '18 at 14:55
  • It turns out my original answer was completely wrong as `SerialPort` asynchronous API ignores timeout properties completely. Please check the update. – Leonid Vasilev Mar 15 '19 at 12:26

1 Answers1

4

Read data in a loop after write operation until get a full response. But you need to use synchronous API and Task.Run() as current version of the asynchronous API ignores SerialPort timeout properties completely and CancellationToken in Task based API almost completely.

Excerpt from the SerialPort.ReadTimeout Microsoft Docs that is relevant to SerialPort.BaseStream.ReadAsync() because it uses default implementation Stream.ReadAsync():

This property does not affect the BeginRead method of the stream returned by the BaseStream property.

Example implementation using synchronous API and dynamic timeout properties update:

static byte[] SendMessage(byte[] message, TimeSpan timeout)
{
    // Use stopwatch to update SerialPort.ReadTimeout and SerialPort.WriteTimeout 
    // as we go.
    var stopwatch = Stopwatch.StartNew();

    // Organize critical section for logical operations using some standard .NET tool.
    lock (_syncRoot)
    {
        var originalWriteTimeout = _serialPort.WriteTimeout;
        var originalReadTimeout = _serialPort.ReadTimeout;
        try
        {
            // Start logical request.
            _serialPort.WriteTimeout = (int)Math.Max((timeout - stopwatch.Elapsed).TotalMilliseconds, 0);
            _serialPort.Write(message, 0, message.Length);

            // Expected response length. Look for the constant value from 
            // the device communication protocol specification or extract 
            // from the response header (first response bytes) if there is 
            // any specified in the protocol.
            int count = ...;
            byte[] buffer = new byte[count];
            int offset = 0;
            // Loop until we recieve a full response.
            while (count > 0)
            {
                _serialPort.ReadTimeout = (int)Math.Max((timeout - stopwatch.Elapsed).TotalMilliseconds, 0);
                var readCount = _serialPort.Read(buffer, offset, count);
                offset += readCount;
                count -= readCount;
            }
            return buffer;
        }
        finally
        {
            // Restore SerialPort state.
            _serialPort.ReadTimeout = originalReadTimeout;
            _serialPort.WriteTimeout = originalWriteTimeout;
        }
    }
}

And example usage:

byte[] request = ...;
TimeSpan timeout = ...;

var sendTask = Task.Run(() => SendMessage(request, timeout));
try
{
    await await Task.WhenAny(sendTask, Task.Delay(timeout));
}
catch (TaskCanceledException)
{
    throw new TimeoutException();
}
byte[] response = await sendTask;

You can do similar thing with CancellationToken instance and use CancellationToken.ThrowIfCancellationRequested() between read and write operations but you have to make sure that proper timeouts are set on SerialPort or otherwise Thread pool thread will hang forever possible holding a lock. As far as I know you can't utilize CancellationToken.Register() because there is no SerialPort method to call to cancel an operation.

For more information check:

Leonid Vasilev
  • 11,910
  • 4
  • 36
  • 50