来自 linux 串口的 C Gps nmea 解析器不解析读取缓冲区的最后一行

C Gps nmea parser from linux serial port does not parse the last line of the read buffer

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

#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 可以实现。我增加了睡眠时间,但没有任何变化。 有谁知道我可以做些什么来实现正确的解析?

您的解析思路似乎还不错,但实现起来有一些问题。

我多年前写了一个 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);

DESCRIPTION

The strtok() function breaks a string into a sequence of zero or more nonempty tokens. On the first call to strtok(), the string to be parsed should be specified in str. In each subsequent call that should parse the same string, str must be NULL.

[...]

The strtok_r() function is a reentrant version strtok(). The saveptr argument is a pointer to a char* variable that is used internally by strtok_r() in order to maintain context between successive calls that parse the same string.

示例:

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 table 的值匹配的值序列,它不会添加 '[=45=]' - 读取缓冲区中的终止字节。这是一个问题,因为你 正在使用需要有效字符串的函数。如果读取输入不 包含换行符,strtok 将继续阅读,直到找到 '[=45=]' 并且如果 该字节不存在,它将读取超出限制。这是未定义的 行为。

这样做的第二个问题是 read 不关心 你准备好的字节,你不是在读行,你准备好了 1000 字节 可能包含也可能不包含字符串的内存块。很有可能 该块不包含字符串,因为 /dev/ttyACM2 将生成一个 无尽的流,永远不会向用户发送 '[=45=]'

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

fdopen

#include <stdio.h>

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

The fdopen() function associates a stream with the existing file descriptor, fd. The mode of the stream (one of the values "r", "r+", "w", "w+", "a", "a+") must be compatible with the mode of the file descriptor. The file position indicator of the new stream is set to that belonging to fd, and the error and end-of-file indicators are cleared. Modes "w" or "w+" do not cause truncation of the file. The file descriptor is not dup'ed, and will be closed when the stream created by fdopen() is closed. The result of applying fdopen() to a shared memory object is undefined.

所以我会这样做:

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,这样编译器就不会优化该变量 离开。大多数情况下(参见 1, 2)最好的方法是:

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);
  }
}