如何在C#中实现串口连续读写并带超时处理?

问题描述 投票:0回答:1

我正在 .NET 8.0 上的 C# 中开发 WPF 应用程序,我需要使用 System.IO.Ports v8.0.0 NuGet 包中的 SerialPort 类连续读取和写入串行端口。我有特定的串行端口设置,并且正在发送 16 字节数据包,然后等待接收 36 字节响应或超时。为了澄清这一点,该程序正在与嵌入式设备进行通信,我从计算机发送命令并等待嵌入式设备的状态响应。但是,如果我发送的数据在传输过程中损坏或出错,嵌入式设备可能会丢弃我的命令并且不会发送状态响应。因此,我一直在努力弄清楚如何处理读取操作的超时问题。

串口配置如下:

private void InitializeCommunication()
{
    try
    {
        _port = new()
        {
            PortName = _comPortName,
            BaudRate = 2_000_000,
            Parity = Parity.Odd,
            DataBits = 8,
            StopBits = StopBits.One,
            Handshake = Handshake.None,
            ReadTimeout = 100,
            WriteTimeout = 100
        };

        _port.Open();

        StartPeriodicCommunication();
    }
    catch (Exception e)
    {
        _port.Close();
    }
}

StartPeriodicCommunication 方法如下所示,本质上执行先写后读的通信循环,直到引发异常:

private async void StartPeriodicCommunication()
{
    try
    {
        while (true)
        {
            await SendReceiveRs485Data();
        }
    }
    catch (Exception ex)
    {
        NotificationQueue.Enqueue("Communication Error: " + ex.Message);
        _port.Close();
    }
}

我尝试的读取操作的第一个方法是将取消令牌传递给 SerialPort.BaseStream.ReadAsync() 方法,但如果嵌入式设备没有响应,程序将无限期地卡住等待 ReadAsync 返回。经过进一步调查,似乎在 ReadAsync 达到执行的某个点后,它不再检查取消令牌的状态,导致它永远不会返回,除非引发异常。我尝试过的不成功的方法是这样的:

Memory<byte> buffer = new byte[36];
int numBytesRead = 0;

using (CancellationTokenSource cts = new(100);

while (numBytesRead < buffer.Length && !cts.IsCancellationRequested)
{
    int bytesRead = await _port!.BaseStream.ReadAsync(buffer.Slice(numBytesRead), cts.Token);
    numBytesRead += bytesRead;

    if (cts.IsCancellationRequested)
    {
        throw new OperationCanceledException($"Received {numBytesRead} bytes");
    }
}

因此,作为替代方案,我设计了两种方法来实现带超时的读取操作:

  1. 关闭端口:第一种方法涉及如果读取操作在超时时间内未完成则关闭串行端口。这种方法似乎有效,但我担心由于频繁打开和关闭端口而对性能和资源管理造成潜在影响。我知道这不是理想的方法,因为 SerialPort 在关闭后可能需要一段不确定的时间才能重新打开,但我这种方法背后的想法是找到一种方法让 ReadAsync 返回异常。
Memory<byte> buffer = new byte[36];
int numBytesRead = 0;

using CancellationTokenSource cts = new(100);
cts.Token.Register(() => _port.Close());

try
{
    while (numBytesRead < buffer.Length && !cts.IsCancellationRequested)
    {
        int bytesRead = await _port!.BaseStream.ReadAsync(buffer.Slice(numBytesRead), cts.Token);
        numBytesRead += bytesRead;
    }
}
catch (Exception ex) 
{
    NotificationQueue.Enqueue($"Read Timeout: {ex.Message}");
    await Task.Delay(100);
    _port.Open();
}
  1. 使用Task.WhenAny():第二种方法使用Task.WhenAny()和Task.Delay来实现超时。此方法不需要关闭并重新打开端口,但我不确定未完成的 ReadAsync 任务是否会导致内存泄漏或其他问题。
Memory<byte> buffer = new byte[36];
int numBytesRead = 0;

using CancellationTokenSource cts = new(100);

while (numBytesRead < buffer.Length && !cts.IsCancellationRequested)
{
    var readTask = _port!.BaseStream.ReadAsync(buffer.Slice(numBytesRead), cts.Token);
    var delayTask = Task.Delay(100);

    var completedTask = await Task.WhenAny(readTask, delayTask);
    if (completedTask == readTask)
    {
        int bytesRead = readTask;
        numBytesRead += bytesRead;
    }
    else
    {
        throw new OperationCanceledException($"Read Timeout: {ex.Message}");
    }
}

这些是简化的代码示例,可帮助理解我的观点,因此希望它们不包含错误或歧义,但我正在寻求关于哪种方法更适合生产环境的建议,考虑资源管理、性能和可靠性。这两种方法是否存在我应该注意的潜在陷阱?根据我在这里看到的帖子和 Ben Voigt 的帖子,使用 SerialPort 类似乎一直很痛苦,所以如果有更好的方法在 C# 中处理这种模式,我愿意接受建议。

编辑#1: 我想出了第三种方法,似乎效果很好。如果我达到超时并丢弃输入缓冲区,它似乎会使 ReadAsync() 返回。除非这种做法有问题,否则我认为这满足了我的需求。然而,我仍然对更好的方法持开放态度。我对异步任务有点陌生,所以我不确定如何确保任务终止并释放其所有资源。

Memory<byte> buffer = new byte[36];
int numBytesRead = 0;

using CancellationTokenSource cts = new(100);
cts.Token.Register(() => _port.DiscardInBuffer());

try
{
    while (numBytesRead < buffer.Length && !cts.IsCancellationRequested)
    {
        int bytesRead = await _port!.BaseStream.ReadAsync(buffer.Slice(numBytesRead), cts.Token);
        numBytesRead += bytesRead;
    }
}
catch (Exception ex) 
{
    NotificationQueue.Enqueue($"Read Timeout: {ex.Message}");
}
c# async-await serial-port timeout task
1个回答
0
投票

如果在 Visual Studio 调试选项中启用源链接,您将能够单步执行该代码。我认为 Windows 上没有超时的方法。

如果您想强制超时,在 .NET 8.0 上您可以使用

WaitAsync

© www.soinside.com 2019 - 2024. All rights reserved.