【问题标题】:C Gps nmea parser from linux serial port does not parse the last line of the read buffer来自linux串口的C Gps nmea解析器不解析读取缓冲区的最后一行
【发布时间】:2018-02-04 13:56:48
【问题描述】:

我需要为我正在开发的电路板创建一个 c gps nmea 解析器(Cubietruck with armbian debian jessie 8.0)。根据我在互联网上找到的几个例子,我得出以下结论:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <string.h>

int fd = -1;
int end_of_loop= 0;

void sig_handler(int sig)
{
  if(sig == SIGINT)
  {
    printf("GPS parsing stopped by SIGINT\n");
    end_of_loop = 1;
    close(fd);
  }
}



int main(int argc, char *argv[])
{
  struct termios newt;
  char *nmea_line;
  char *parser;
  double latitude;
  float longitude;
  float altitude;

  signal(SIGINT, sig_handler);

  fd = open("/dev/ttyACM2", O_RDWR | O_NONBLOCK);
  if (fd >= 0)
  {
    tcgetattr(fd, &newt);
    newt.c_iflag &= ~IGNBRK;         
    newt.c_iflag &= ~(IXON | IXOFF | IXANY); 
    newt.c_oflag = 0;               

    newt.c_cflag |= (CLOCAL | CREAD);               
    newt.c_cflag |= CS8;                       
    newt.c_cflag &= ~(PARENB | PARODD);         
    newt.c_cflag &= ~CSTOPB;                   

    newt.c_lflag = 0;                            

    newt.c_cc[VMIN]  = 0; 
    newt.c_cc[VTIME] = 0; 
    tcsetattr(fd, TCSANOW, &newt);



  usleep(100000);

  while(end_of_loop == 0)
  {

    char read_buffer[1000];
    read(fd, &read_buffer,1000);
    //printf("|%s|", r_buf);

    nmea_line = strtok(read_buffer, "\n");

    while (nmea_line != NULL)
    {

      parser = strstr(nmea_line, "$GPRMC");
      if (parser != NULL)
      {
        printf("|%s| \n", nmea_line);
        char *token = strtok(nmea_line, ",");
        int index = 0;
        while (token != NULL)
        {
          if (index == 3)
          {
            latitude = atof(token);
            printf("found latitude: %s %f\n", token, latitude);
          }
          if (index == 5)
          {
            longitude = atof(token);
            printf("found longitude: %s %f\n", token, longitude);
          }
          token = strtok(NULL, ",");
          index++;
        }
      }

      parser = strstr(nmea_line, "$GPGGA");
      if (parser != NULL)
      {
        printf("|%s| \n", nmea_line);
        char *token = strtok(nmea_line, ",");
        int index = 0;
        while (token != NULL)
        {
          if (index == 13)
          {
            altitude = atof(token);
            printf("found altitude: %s %f\n", token, altitude);
          }
          token = strtok(NULL, ",");
          index++;
        }

      }



      printf("|%s| \n", nmea_line);
      nmea_line = strtok(NULL, "\n");
    }

    usleep(500000);

  }

  close(fd);

  return 0;

  }
  else
  {
    printf("Port cannot be opened");
    return -1;
  }
}

目前我测试没有 GPS 定位的负面情况。 这种情况下串行端口的输出是每次读取的:

$GNGNS,,,,,,NNN,,,,,,*1D
$GPVTG,,T,,M,,N,,K,N*2C
$GPGSA,A,1,,,,,,,,,,,,,,,*1E
$GNGSA,A,1,,,,,,,,,,,,,,,*00
$GPGGA,,,,,,0,,,,,,,,*66
$GPRMC,,V,,,,,,,,,,N*53

当我运行代码时,我得到了 GPGGA 的解析打印输出,而不是 GPRMC:

GNGNS,,,,,,NNN,,,,,,*1D
| GPVTG,,T,,M,,N,,K,N*2C
| GPGSA,A,1,,,,,,,,,,,,,,,*1E
| GNGSA,A,1,,,,,,,,,,,,,,,*00
| GPGGA,,,,,,0,,,,,,,,*66
|$GPGGA|
| GNGNS,,,,,,NNN,,,,,,*1D
| GNGNS,,,,,,NNN,,,,,,*1D
| GPVTG,,T,,M,,N,,K,N*2C
| GPGSA,A,1,,,,,,,,,,,,,,,*1E
| GNGSA,A,1,,,,,,,,,,,,,,,*00
| GPGGA,,,,,,0,,,,,,,,*66
|$GPGGA|

我认为这与 GPRMC 在最后一行的事实有关,当执行 nmea_line = strtok(NULL, "\n"); 时 nmea_lime 变为 NULL。我用strcat 在read_buffer 上添加了一个虚拟行,但没有成功。
我打印了索引,我发现对于 GPGGA,只有 index = 3 被实现。我增加了睡眠时间,但没有任何变化。 有谁知道我可以做些什么来实现正确的解析?

【问题讨论】:

  • 不要使用 strtok()。它无法识别连续的分隔符。
  • @wildplasser 我可以用什么代替?
  • 你可以使用 strchr(),或者手动比较字符。最好是构建一个小型 DFA。(您的代码已经非常接近 DFA)
  • 另外:read(fd, &amp;read_buffer,1000); 不考虑换行符(并且缓冲区不会被 NUL 终止!)。您还必须找到 '\n' 字符才能同步到行尾/下一行的开头。

标签: c linux serial-port nmea


【解决方案1】:

您的解析想法似乎还可以,但实现时存在一些问题。

多年前我写了一个 gps nmea 解析器,据我所知, 以"\r\n" 结尾的行似乎也是如此,因为对于这一行

printf("|%s| \n", nmea_line);

你得到

| GPVTG,,T,,M,,N,,K,N*2C

如果你改成

printf(">%s| \n", nmea_line);

你很可能会看到

< GNGNS,,,,,,NNN,,,,,,*1D

第二个问题是您以可重入的方式使用strtok。在 循环的开始,你做nmea_line = strtok(read_buffer, "\n");。然后 你去解析行并进入一个新的循环。然后你做东西线 char *token = strtok(nmea_line, ","); 并通过这样做 strtok 忘记了 关于第一次通话的信息​​。

在你所做的一切结束时再次nmea_line = strtok(NULL, "\n");,但是 这个NULL 适用于哪个strtok?根据输出,你永远不会 知道,但它肯定不会与 nmea_line = strtok(read_buffer, "\n"); 匹配。

幸运的是,strtok 有一个可重入版本:strtok_r

man strtok

#include <string.h>

char *strtok(char *str, const char *delim);

char *strtok_r(char *str, const char *delim, char **saveptr);

描述

strtok() 函数将字符串分成零个或多个非空标记的序列。在第一次调用strtok() 时,字符串为 解析的应该在str 中指定。在应该解析相同字符串的每个后续调用中,str 必须是 NULL

[...]

strtok_r() 函数是 strtok() 的可重入版本。 saveptr 参数是一个指向 char* 的指针 strtok_r() 内部使用的变量为了维护连续调用之间的上下文 解析相同的字符串。

例子:

char line[] = "a:b:c,d:e:f,x:y:z";

char *s1, *s2, *token1, *token2, *in1, *in2;

in1 = line;

while(token1 = strtok_r(in1, ",", &s1))
{
    in1 = NULL; // for subsequent calls

    in2 = token1;

    printf("First block: %s\n", token1);

    while(token2 = strtok_r(in2, ":", &s2))
    {
        in2 = NULL; // for subsequent calls

        printf("  val: %s\n", token2);
    }
}

输出:

First block: a:b:c
  val: a
  val: b
  val: c
First block: d:e:f
  val: d
  val: e
  val: f
First block: x:y:z
  val: x
  val: y
  val: z

我看到的另一个问题是:

while(...)
{
    read(fd, &read_buffer,1000);

    nmea_line = strtok(read_buffer, "\n");
}

read 函数与fgets 不同,它不读取字符串,它读取字节。那 意味着read 不在乎它在读什么。如果序列恰好是 与 ASCII 表的值匹配的值序列,它不会添加 '\0'- 读取缓冲区中的终止字节。这是一个问题,因为你 正在使用需要有效字符串的函数。如果读取输入不 包含换行符,strtok 将继续阅读,直到找到 '\0' 并且如果 该字节不存在,它将超出限制读取。这是未定义的 行为。

这样做的第二个问题是read 再次不在乎 你准备好了的字节,你没有读行,你准备好了 1000 字节 可能包含或不包含字符串的内存块。最有可能的是 该块不包含字符串,因为/dev/ttyACM2 将生成一个 源源不断,从不向用户发送'\0'

我会使用fgets 获取一行并解析它,然后再获取另一行,然后 很快。因为你只有文件描述符,你应该使用:

fdopen

#include <stdio.h>

FILE *fdopen(int fd, const char *mode);

fdopen() 函数将流与现有文件描述符 fd 相关联。 流的mode(值"r""r+""w""w+""a""a+" 之一) 必须与文件描述符的模式兼容。 新流的文件位置指示符设置为属于fd 的位置指示符, 并且错误和文件结束指示符被清除。模式 "w""w+" 不会导致 文件的截断。文件描述符没有被复制,并且会在 fdopen() 创建的流已关闭。申请fdopen()的结果 到共享内存对象是未定义的。

所以我会这样做:

FILE *f = fopen(fd, "r");


// the gps nmea lines are never that long
char line[64];

char *t1_save;

while(fgets(line, sizeof line, f))
{
    // get rid of \r\n
    line[strcspn(line, "\r\n")] = 0;

    parser = strstr(line, "$GPRMC");
    if(parser)
    {
        // do the parsing
    }

    ...
}

在这个版本中,您甚至不需要strtok_r,因为您不需要 嵌套strtok 调用。


编辑

我之前错过了一件事:

int end_of_loop= 0;

void sig_handler(int sig)
{
  if(sig == SIGINT)
  {
    printf("GPS parsing stopped by SIGINT\n");
    end_of_loop = 1;
    close(fd);
  }
}

int main(int argc, char *argv[])
{
    ...
    while(end_of_loop == 0)
    {
    }
}

根据您的编译器的优化,您最终会陷入无休止的境地 循环,即使按下 Ctrl+C。编译器可能 将while 循环优化为while(1),因为在mainend_of_loop 变量永远不会改变,所以没有必要总是检查那个值。

当试图通过捕获信号来停止循环时,最好至少声明 变量为volatile,因此编译器不会优化该变量 离开。大多数情况下(参见12)最好的方法是:

volatile sig_atomic_t end_of_loop= 0;

void sig_handler(int sig)
{
  if(sig == SIGINT)
  {
    printf("GPS parsing stopped by SIGINT\n");
    end_of_loop = 1;
    close(fd);
  }
}

【讨论】:

  • line[strcspn(line, "\r\n")] = 0; 将在一次扫描中完成。
  • @Pablo 通过回答感谢您的一百万。我是 linux 串口编程的新手。当您提到“在此版本中,您甚至不需要 strtok_r”时,一个问题是您的意思是我可以按原样使用我的代码,还是我应该做其他事情?
  • @dk13 你不能在不修复代码的情况下使用它。我回答的重点是告诉您哪些部分是错误的以及您将如何修复它。使用 fgets 您不需要使用 strtok 来查找换行符,因此您不必嵌套 strtok 调用。请仔细阅读答案,你会明白的。
  • @wildplasser 是的,我知道。可悲的是,我过去有一些设备在中游时只发送换行符而不是\r,从而弄乱了解析器。有时型号为“0.1”的设备会返回\r\n,但当客户多年后购买型号为“0.5”的设备时,客户打电话给我说“程序不起作用”。经过仔细检查,制造商更改了协议,现在只发送换行符。这就是为什么我对所有这些问题分别进行处理。
  • @wildplasser 胡?你没有冒犯我,我承认你的解释比我之前写的要好。我知道 strcspn 的语义,但如果它们出现在行中,我想去掉both。但考虑到这一点,我意识到这没有必要,所以我更新了我的帖子。
猜你喜欢
  • 2010-10-09
  • 2018-11-21
  • 2021-11-13
  • 1970-01-01
  • 1970-01-01
  • 2020-04-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多