Async Serial Port with CancellationToken and ReadTimeout
Asked Answered
H

2

4

I'm trying to wrap the SerialPort's read method in a task that can be awaited, that way I can get the benefits of using a CancellationToken and the timeout from the SerialPort object. My issue is that I cannot seem to get the Task to throw a CancellationException. Here's my code...

    static CancellationTokenSource Source = new CancellationTokenSource();

    static void Main(string[] args)
    {
        TestAsyncWrapperToken();
        Console.WriteLine("Press any key to cancel");
        Console.ReadKey(true);
        Source.Cancel();
        Console.WriteLine("Source.Cancel called");
        Console.ReadLine();
    }

    static async void TestAsyncWrapperToken()
    {
        try
        {
            using (var Port = new SerialPort("COM2", 9600, Parity.None, 8, StopBits.One))
            {
                Port.Open();
                var Buffer = new byte[1];
                await Task.Factory.StartNew(() =>
                {
                    Console.WriteLine("Starting Read");
                    Port.ReadTimeout = 5000;
                    Port.Read(Buffer, 0, Buffer.Length);                        
                }, Source.Token);
            }
        }
        catch (TaskCanceledException)
        {
            Console.WriteLine("Task Cancelled");
        }
        catch (TimeoutException)
        {
            Console.WriteLine("Timeout on Port");
        }
        catch (Exception Exc)
        {
            Console.WriteLine("Exception encountered {0}", Exc);
        }
    }

Is it because the Port.Read method is a blocking call? Any suggestions?

Hajj answered 8/12, 2017 at 21:24 Comment(0)
S
1

Two methods are conceivable.

  1. Using ReadAsync
    It is to get the Stream object from the BaseStream property of the SreiaPort object and use the Stream.ReadAsync Method (Byte[], Int32, Int32, CancellationToken).

    Although it is not exactly matched content, please refer to this.
    How to cancel Stream.ReadAsync?
    NetworkStream.ReadAsync with a cancellation token never cancels

  2. Using DataReceivedEvent and SerialDataReceivedEventHandler
    Change so that it works with DataReceivedEvent as trigger.
    Please refer to the answer of this article.
    Sample serial port comms code using Async API in .net 4.5?

p.s.
If you want to work with .NET 4.0 or lower, the following article will be helpful.
It will also be related knowledge to the above.
If you must use .NET System.IO.Ports.SerialPort
Reading line-by-line from a serial port (or other byte-oriented stream)

Scandinavia answered 18/12, 2017 at 11:44 Comment(0)
M
0

I could only find solutions with exception and closed stream afterwards, so here is my solution as an extension method, which just returns null on cancel or timeout:

public static async Task<byte[]> ReadBytesAsync(this SerialPort serialPort, 
    CancellationToken cancel, int maxLen = 256, int timeoutInSeconds = 5, byte stop = (byte)'\r')
{
    const int delay = 50;
    int read, i = 0, timeout = timeoutInSeconds * 1000 / delay;
    byte[] buffer = new byte[maxLen];
    while (true) {
        if (serialPort.BytesToRead == 0) {
            await Task.Delay(delay);
            timeout--;
        }
        else {
            i += read = await serialPort.BaseStream.ReadAsync(buffer, i, maxLen - i);
            if (i == maxLen || Array.IndexOf(buffer, stop, i - read, read) >= 0) break;
        }
        if (timeout == 0 || cancel.IsCancellationRequested) return null;
    }
    Array.Resize(ref buffer, i);
    return buffer;
}

I had to learn that the ReadAsync() implementation doesn't use the CancellationToken parameter, and also not the ReadTimeout setting, so it would hang forever if no (more) data arrives and could only be cancelled by forcing an error on the stream i.e.
using (cancel.Register(() => serialPort.DiscardInBuffer())) { }.

I think the better way is simply not to call it until bytes are received.

Of course my implementation is a bit special, for performance reasons it doesn't read byte by byte, but returns when the stop byte is "somewhere". You should adapt this to your needs.

Bonus: need a string? serialPort.Encoding.GetString(bytes, 0, bytes.Length);

Monastic answered 12/9, 2023 at 20:47 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.