C# await event and timeout in serial port communication

天大地大妈咪最大 提交于 2019-12-29 02:09:28

问题


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?


回答1:


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:

  • Top 5 SerialPort Tips article by Kim Hamilton
  • Recommended asynchronous usage pattern of SerialPort, Document that CancellationToken in Stream.ReadAsync() is advisory and NetworkStream.ReadAsync/WriteAsync ignores CancellationToken related issues on .NET GitHub
  • Should I expose asynchronous wrappers for synchronous methods? article by Stephen Toub


来源:https://stackoverflow.com/questions/53335736/c-sharp-await-event-and-timeout-in-serial-port-communication

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!