我正在 .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");
}
}
因此,作为替代方案,我设计了两种方法来实现带超时的读取操作:
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();
}
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}");
}
如果在 Visual Studio 调试选项中启用源链接,您将能够单步执行该代码。我认为 Windows 上没有超时的方法。
WaitAsync
。