C#串口通信中的await事件和超时

Posted

技术标签:

【中文标题】C#串口通信中的await事件和超时【英文标题】:C# await event and timeout in serial port communication 【发布时间】: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 完全忽略了超时属性。请检查更新。 【参考方案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 方法可以调用来取消操作。

更多信息请查看:

Top 5 SerialPort Tips Kim Hamilton 的文章 .NET GitHub 上的Recommended asynchronous usage pattern of SerialPort、Document that CancellationToken in Stream.ReadAsync() is advisory 和NetworkStream.ReadAsync/WriteAsync ignores CancellationToken 相关问题 Should I expose asynchronous wrappers for synchronous methods?Stephen Toub 的文章

【讨论】:

以上是关于C#串口通信中的await事件和超时的主要内容,如果未能解决你的问题,请参考以下文章

c#如何实现串口通信读取数据

C#中的串口通信SerialPort

C#串口读取?

Unity串口通信接受和发送数据C#

c#如何串口通信连接三菱plc?

LabVIEW串口通信