【问题标题】:C# await event and timeout in serial port communicationC#串口通信中的await事件和超时
【发布时间】:2018-11-16 10:17:21
【问题描述】:

您好,我在串口上有一个简单的通信,一切都根据书和文档,所以开放端口方法如下所示:

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

这里我们有一个关于数据读取的事件:

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

一切都很好,花花公子,但现在我想按需发送消息并将响应存储在属性中,我还想在该任务超时时添加取消令牌。所以我想出了这个方法:

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

问题在于这个while循环,如果它是任务,它会进入无限循环,并且它不会捕获超时令牌,如果我将它放在任务之外并删除工作人员它可以工作,但我会丢失取消令牌。我想我可以做一些手动倒计时,比如:

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

但它看起来很丑。

所以我认为我的基本问题是如何有效地等待事件响应并获得超时?

【问题讨论】:

  • while (!isFinished) { }ManualEventResetSlim ...更多的是Wait(timeout) ...
  • 异常处理是我最讨厌的事情,而你的处理并不理想。对于初学者,您将在发生致命异常后继续操作,并且没有正确记录/公开内容。这里有两篇关于我经常链接的材料的文章:blogs.msdn.microsoft.com/ericlippert/2008/09/10/… | codeproject.com/Articles/9538/…
  • isFinishedbool 更改为 ManualEventResetSlim 就像 ManualEventResetSlim isFinished=ManualEventResetSlim (); 而不是将其设置为 false/true 使用 Reset/Set 然后改为 await worker 使用 isFinished.Wait(Timeout)
  • 你知道需要多少字节来响应吗?
  • 原来我的原始答案完全错误,因为SerialPort 异步 API 完全忽略了超时属性。请检查更新。

标签: c# serial-port


【解决方案1】:

写操作后循环读取数据,直到获得完整响应。但是您需要使用同步 API 和 Task.Run(),因为当前版本的异步 API 完全忽略了 SerialPort 超时属性,而在基于任务的 API 中几乎完全忽略了 CancellationToken

摘自与SerialPort.BaseStream.ReadAsync() 相关的SerialPort.ReadTimeout Microsoft Docs,因为它使用默认实现Stream.ReadAsync()

此属性不影响BaseStream 属性返回的流的BeginRead 方法。

使用同步 API 和动态超时属性更新的示例实现:

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

以及示例用法:

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;

您可以对CancellationToken 实例做类似的事情,并在读写操作之间使用CancellationToken.ThrowIfCancellationRequested(),但您必须确保在SerialPort 上设置了适当的超时,否则线程池线程将永远挂起,可能持有锁.据我所知,您不能使用CancellationToken.Register(),因为没有SerialPort 方法可以调用来取消操作。

更多信息请查看:

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多