从串口解析/格式化数据 - C#
Posted
技术标签:
【中文标题】从串口解析/格式化数据 - C#【英文标题】:Parsing/formatting data from serial port - C# 【发布时间】:2012-02-29 19:14:26 【问题描述】:我开发了一个监听串口的小程序。我的程序正在接收数据。问题是,它没有以所需的格式(一个字符串)显示它。我的程序接收的数据有两个字符串,例如:
ID:34242 State:NY
邮编:12345 StreetType:Ave
它按块显示,并且一些数据被传递到下一行:
ID:34242
State:N
Y Zip:12
345 Street
Type:Ave
我已经使用 SerialDataReceive 事件处理程序来接收我的数据,它看起来像这样:
private static void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
SerialPort spL = (SerialPort) sender;
int bufSize = 20;
Byte[] dataBuffer = new Byte[bufSize];
Console.WriteLine("Data Received at"+DateTime.Now);
Console.WriteLine(spL.Read(dataBuffer, 0, bufSize));
string s = System.Text.ASCIIEncoding.ASCII.GetString(dataBuffer);
Console.WriteLine(s);
如您所见,我将字节检索到缓冲区中,创建一个字节数组来保存数据并使用 ASCII 编码将字节转换为字符串。我尝试使用 ReadLine() 但我的数据没有通过该函数显示。有谁知道将数据解析和格式化为一个字符串的任何其他方法?
【问题讨论】:
每条记录的末尾是否有分隔符?打印出数字字节值,并特别注意发送的最后一条记录。数据以连续流的形式出现,但希望协议设计为带有某种终止字符(回车是典型的,但可能不是这种情况,因为 ReadLine 不适合您)。 【参考方案1】:问题在于,您可能已经猜到,一旦通过串行端口接收到数据,就会引发事件 DataReceived。那里可能没有完整的记录; SerialPort 对象不知道您认为哪些数据“足够”重要或可行。
通常的解决方案是维护另一个接收数据的“缓冲区”,其中包含您认为不完整的任何数据。当数据通过端口传入并且您的事件触发时,它应该首先获取缓冲区中的内容并将其附加到您已经收到的内容中。然后,您应该从这个数据缓冲区的开头开始检查接收到的数据,寻找对您有意义的数据原子“块”的已知模式;例如,假设您收到的第一件事是"ID: 12"
。您将其放入缓冲区,然后扫描缓冲区以查找由正则表达式 "ID: \d*? "
定义的模式。因为缓冲区中不存在尾随空格,所以您的模式无法找到任何有意义的东西,因此您现在知道您没有收到完整的消息。
然后,在下一次引发 DataReceived 事件时,将"453 Sta"
拉出串行缓冲区。你将它附加到你已经拥有的东西上并得到"ID:12453 Sta"
,当你应用正则表达式时,你会得到匹配“ID:12345”。您将其传递给进一步处理的方法(可能显示到控制台),并从缓冲区的前面删除相同的字符串,留下“Sta”。再次扫描你没有发现任何其他感兴趣的东西,所以你留下你所拥有的,循环重复 aws 数据继续进来。显然,你将测试更多的模式,而不仅仅是 ID 模式;您可以搜索您希望收到的整个“字符串”,例如"ID: \d*? State: \w2 "
。您甚至可以将数据保留在缓冲区中,直到您将两个字符串都记录下来:"ID:\d*? State:\w2 Zip:\d5 StreetType:\w*? "
。
无论哪种方式,您都需要确定接收到的数据是可靠的“固定长度”(意味着特定类型的每个字符串始终具有相同数量的字节或字符),还是可靠的“定界”(意味着会有一些字符或字符组合总是分隔数据的重要元素)。如果这些都不适用,则可能很难将数据解析为单字段块。
这是一个基于您已有的示例:
private static StringBuilder receiveBuffer = new StringBuilder();
private static void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
SerialPort spL = (SerialPort) sender;
int bufSize = 20;
Byte[] dataBuffer = new Byte[bufSize];
Console.WriteLine("Data Received at"+DateTime.Now);
Console.WriteLine(spL.Read(dataBuffer, 0, bufSize));
string s = System.Text.ASCIIEncoding.ASCII.GetString(dataBuffer);
//here's the difference; append what you have to the buffer, then check it front-to-back
//for known patterns indicating fields
receiveBuffer.Append(s);
var regex = new Regex(@"(ID:\d*? State:\w2 Zip:\d5 StreetType:\w*? )");
Match match;
do
match = regex.Match(receiveBuffer.ToString());
if(match.Success)
//"Process" the significant chunk of data
Console.WriteLine(match.Captures[0].Value);
//remove what we've processed from the StringBuilder.
receiveBuffer.Remove(match.Captures[0].Index, match.Captures[0].Length);
while (match.Success);
【讨论】:
感谢您的建议。我尝试将此添加到我的代码中。不幸的是,我看到的只是每块数据的字节数。【参考方案2】:参见提示 #1
http://blogs.msdn.com/b/bclteam/archive/2006/10/10/top-5-serialport-tips-_5b00_kim-hamilton_5d00_.aspx
使用 SerialPort.Read(buffer, offset, count) 时,其中 count 是 要读取的字节数,检查返回值,它告诉 你实际读取的字节数。开发人员有时会假设 读取完成时将返回计数字节/字符。这就是 阅读确实如此。如果串行端口上有可用的字节, 读取最多返回 count 个字节,但不会阻塞剩余的字节 字节。如果串行端口上没有可用字节,Read 将 阻塞,直到端口上至少有一个字节可用,直到 ReadTimeout 毫秒已经过去,此时 a 将抛出 TimeoutException。要在您的代码中解决此问题,请检查 处理时实际读取并使用该值的字节数 返回数据。
基本上,您不能保证获得 count 个字节。您将获得可读取的内容,最多 count 个字节 - 不超过 count,但可能更少。
【讨论】:
那么,根据这句话的意思,您需要知道缓冲区中的确切字节数才能按需要显示或处理?如果是这样,当每个数据块具有不同的字节数时,我将如何实现? 在您的示例代码中,Read 方法返回读取的字节数,以便您查看并采取相应措施。如果您期望 20 并得到 10,您可以继续读取和构建数据,直到最终有 20 个字节。如果您的数据不是固定长度的,那么您将不得不继续查看到目前为止收到的内容,以确定您是否拥有完整的数据块。 请注意,Read 方法需要一个偏移量。这对这个问题很有用。 大家好,我想出了解决方案。我以一种允许我收集所有字节 (37) 的方式重建我的 SerialDataReceive 事件处理程序,读取所有这些字节并将它们转换为字符串。我现在看到程序中的数据与从端口发送的数据完全相同。谢谢大家的快速回复。他们对我帮助很大。【参考方案3】:假设没有终止字符,这样的事情可能会起作用。棘手的部分是确定何时打印新行。
您可以尝试在每个 ID:
之前插入换行符(例如,将 "ID:"
替换为 "\r\n\ID:"
)。当您先收到StreetType:AveI
然后再收到"D:23566 St"
时,这有时仍然会失败。要解决此问题,您可以在StreetType:
之后查找任何I
,但这也不像听起来那么容易——如果您看到345 Stre
、etTy
、pe:RdI
会怎样。另外,如果I
是有效字符(tType:DRI
,VE ID:23525
)怎么办?
我认为以下代码应该正确处理这些情况。请注意,我从Console.WriteLine
切换到Console.Write
并在需要时手动添加新行:
private static var previousStringPerPort = new Dictionary<SerialPort,string>();
private static void Port_DataReceived(object sender,
SerialDataReceivedEventArgs e)
SerialPort spL = (SerialPort) sender;
int bufSize = 20;
Byte[] dataBuffer = new Byte[bufSize];
Console.WriteLine("Data Received at"+DateTime.Now);
Console.WriteLine(spL.Read(dataBuffer, 0, bufSize));
if (!previousStringPerPort.ContainsKey(spL))
previousStringPerPort[spL] = "";
string s = previousStringPerPort[spL] +
System.Text.ASCIIEncoding.ASCII.GetString(dataBuffer);
s = s.Replace("ID:",Environment.NewLine + "ID:");
if (s.EndsWith("I"))
previousStringPerPort[spL] = "I";
s = s.Remove(s.Length-1);
else if (s.EndsWith("ID"))
previousStringPerPort[spL] = "ID";
s = s.Remove(s.Length - 2);
Console.Write(s);
现在剩下的唯一问题是,如果最后一条记录确实以I
或ID
结尾,它将永远不会被打印。定期超时刷新前一个字符串可以解决这个问题,但它会引入(许多)更多问题。
【讨论】:
谢谢贾斯汀。没有回车。我会试试你上面推荐的。这看起来像是定义行的开头和结尾的好方法,这正是我所需要的。以上是关于从串口解析/格式化数据 - C#的主要内容,如果未能解决你的问题,请参考以下文章