从串口解析/格式化数据 - 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 StreetType: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);

现在剩下的唯一问题是,如果最后一条记录确实以IID 结尾,它将永远不会被打印。定期超时刷新前一个字符串可以解决这个问题,但它会引入(许多)更多问题。

【讨论】:

谢谢贾斯汀。没有回车。我会试试你上面推荐的。这看起来像是定义行的开头和结尾的好方法,这正是我所需要的。

以上是关于从串口解析/格式化数据 - C#的主要内容,如果未能解决你的问题,请参考以下文章

GPS串口报文NMEA格式,Python解析L76-gps数据流

GPS数据解析

从奇怪的格式解析C#中的DateTime

DateTime.Parse 美国日期格式 C#

C# 之 串口数据侦听的实现

C# 串口操作系列 -- 协议篇,文本协议数据解析