串行端口 - C#:与设备通信期间数据缓冲区的奇怪偏移
本文关键字:缓冲区 数据 通信 串行端口 | 更新日期: 2023-09-27 17:55:52
在我的应用程序中,我必须从通过COM端口连接的设备接收和处理一些数据。我部分地这样做。在该特定设备中,前两个字节是数据包的长度(减去2,因为它没有考虑这两个字节;所以它毕竟是数据包其余部分的长度)。然后,由于我知道设备倾向于缓慢地向我发送数据,所以我在循环中读取数据包的其余部分,直到读取所有数据。但就在这里,我遇到了奇怪的问题。让我们假设整个数据包(包括前两个长度的字节)看起来像这样:['a', 'b', 'c', 'd', 'e']。当我读取前两个字节("a"和"b")时,我希望数据包的其余部分看起来像这样:['c', 'd', 'e']。但相反,它看起来像这样:['b', 'c', 'd', 'e']。为什么响应的第二个字节仍在读取缓冲区中?为什么只有第二个,而没有前一个?
下面的代码显示了如何处理通信过程:
//The data array is some array with output data
//The size array is two-byte array to store frame-length bytes
//The results array is for device's response
//The part array is for part of the response that's currently in read buffer
port.Write(data, 0, data.Length);
//Receiving device's response (if there's any)
try
{
port.Read(size, 0, 2); //Read first two bytes (packet's length) of the response
//We'll store entire response in results array. We get its size from first two bytes of response
//(+2 for these very bytes since they're not counted in the device's data frame)
results = new byte[(size[0] | ((int)size[1] << 8)) + 2];
results[0] = size[0]; results[1] = size[1]; //We'll need packet size for checksum count
//Time to read rest of the response
for(offset = 2; offset < results.Length && port.BytesToRead > 0; offset += part.Length)
{
System.Threading.Thread.Sleep(5); //Device's quite slow, isn't it
try
{
part = new byte[port.BytesToRead];
port.Read(part, 0, part.Length); //Here's where old data is being read
}
catch(System.TimeoutException)
{
//Handle it somehow
}
Buffer.BlockCopy(part, 0, results, offset, part.Length);
}
if(offset < results.Length) //Something went wrong during receiving response
throw new Exception();
}
catch(Exception)
{
//Handle it somehow
}
你犯了一个传统的错误,你不能忽略 Read() 的返回值。 它告诉您实际接收了多少字节。 它将至少为 1,不超过计数。 然而,接收缓冲区中存在许多,BytesToRead告诉你。 只需继续调用 Read(),直到您满意为止:
int cnt = 0;
while (cnt < 2) cnt += port.Read(size, cnt, 2 - cnt);
只需在代码的第二部分使用相同的代码,这样您就不会在没有 Sleep() 调用的情况下烧毁 100% 的核心。 请记住,当您读取大小时,TimeoutException 的可能性也很大,实际上更有可能。 如果在 cnt> 0 时抛出它,则这是一个致命的异常,您无法再重新同步。
好吧,很奇怪,但是当我分开读取前两个字节时:
port.Read(size, 0, 1); //Read first two bytes (packet's length) of the response
port.Read(size, 1, 1); //Second time, lol
无论我从设备接收哪种数据包,一切都很好。
SerialPort 的文档包含以下文本:
因为 SerialPort 类缓冲数据,并且流包含在 BaseStream 属性没有,两者可能会在如何 许多字节可供读取。属性可以 指示有要读取的字节,但这些字节可能不是 可访问 BaseStream 属性中包含的流,因为 它们已缓冲到 SerialPort 类。
这能解释为什么BytesToRead
给你令人困惑的价值观吗?
就个人而言,我总是使用 DataReceived
事件,在我的事件处理程序中,我使用 ReadExisting()
读取所有立即可用的数据并将其添加到我自己的缓冲区中。我不试图在该级别对数据流施加任何意义,我只是缓冲它;相反,我通常会编写一个小的状态机,一次从缓冲区中取出一个字符,并将数据解析为所需的任何格式。
或者,您可以使用 ReactiveExtensions 生成接收字符的可观察序列,然后在该序列上分层观察者。我用几个扩展方法做到这一点,比如:
public static class SerialObservableExtensions
{
static readonly Logger log = LogManager.GetCurrentClassLogger();
/// <summary>
/// Captures the <see cref="System.IO.Ports.SerialPort.DataReceived" /> event of a serial port and returns an
/// observable sequence of the events.
/// </summary>
/// <param name="port">The serial port that will act as the event source.</param>
/// <returns><see cref="IObservable{Char}" /> - an observable sequence of events.</returns>
public static IObservable<EventPattern<SerialDataReceivedEventArgs>> ObservableDataReceivedEvents(
this ISerialPort port)
{
var portEvents = Observable.FromEventPattern<SerialDataReceivedEventHandler, SerialDataReceivedEventArgs>(
handler =>
{
log.Debug("Event: SerialDataReceived");
return handler.Invoke;
},
handler =>
{
// We must discard stale data when subscribing or it will pollute the first element of the sequence.
port.DiscardInBuffer();
port.DataReceived += handler;
log.Debug("Listening to DataReceived event");
},
handler =>
{
port.DataReceived -= handler;
log.Debug("Stopped listening to DataReceived event");
});
return portEvents;
}
/// <summary>
/// Gets an observable sequence of all the characters received by a serial port.
/// </summary>
/// <param name="port">The port that is to be the data source.</param>
/// <returns><see cref="IObservable{char}" /> - an observable sequence of characters.</returns>
public static IObservable<char> ReceivedCharacters(this ISerialPort port)
{
var observableEvents = port.ObservableDataReceivedEvents();
var observableCharacterSequence = from args in observableEvents
where args.EventArgs.EventType == SerialData.Chars
from character in port.ReadExisting()
select character;
return observableCharacterSequence;
}
}
ISerialPort
接口只是我从 SerialPort
类中提取的一个标头接口,这使我在进行单元测试时更容易模拟它。