来自Linux串行端口的C Gps nmea解析器无法解析读取缓冲区的最后一行 [英] C Gps nmea parser from linux serial port does not parse the last line of the read buffer

查看:150
本文介绍了来自Linux串行端口的C Gps nmea解析器无法解析读取缓冲区的最后一行的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我需要为我正在开发的板(带有armbian debian jessie 8.0的Cubietruck)创建一个c gps nmea解析器.根据我在互联网上发现的几个示例,我得出以下结论:

I need to create a c gps nmea parser for a board I'm working on (Cubietruck with armbian debian jessie 8.0) . Based on several examples I found on the internet I concluded in the following:

#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修复的否定情况. 在这种情况下,串行端口的输出是每次读取:

For the time being I test negative case where there is no GPS fix. The output from the serial port for that case is for every read :

$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:

When I run the code I get parse print outs for the GPGGA but not for the 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,仅索引= 3.我增加了睡眠时间,但没有任何变化. 有谁知道我可以做些什么来实现正确的解析?

I assume that has something to do with the fact that GPRMC is on the last line and when nmea_line = strtok(NULL, "\n"); is executed then the nmea_lime becomes NULL. I added a dummy line on the read_buffer with strcat but with no success.
I printed the index and I found that for GPGGA only index = 3 is achieved. I increased the usleep time but there was no change. Has anyone any idea what I can do to achieve correct parsing?

推荐答案

您的解析想法似乎还可以,但是实现有一些问题.

Your idea of parsing seems OK, but the implementation has a few problems though.

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

I wrote many years ago a gps nmea parser, and as far as I can remember, the lines ended with "\r\n", that would seem also the case because for this line

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会忘记 有关第一次通话的信息​​.

A second problem is that you are using strtok in a reentrant kind of way. At the beginning of the loop, you do nmea_line = strtok(read_buffer, "\n");. Then you go to parse the line and enter a new loop. Then you do stuff line char *token = strtok(nmea_line, ","); and by doing this strtok forgets the information about the first call.

最后,您再次执行nmea_line = strtok(NULL, "\n");,但是 此NULL适用于哪个strtok?取决于输出,您将永远不会 知道,但不一定会与nmea_line = strtok(read_buffer, "\n");相匹配.

At the end of everything you do again nmea_line = strtok(NULL, "\n");, but this NULL applies to which strtok? Depending on the output you will never know but it won't certainly match to with to nmea_line = strtok(read_buffer, "\n");.

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

Luckly there is a reentrant version of 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.

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.

[...]

strtok_r()函数是可重入版本strtok(). saveptr参数是指向char*的指针 strtok_r() 在内部使用的变量,以保持连续调用之间的上下文 解析相同的字符串.

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

我看到的另一个问题是:

Another problem that I see is this:

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

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

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

The read function, unlike fgets, does not read strings, it reads bytes. That means that read doesn't care what it's reading. If the sequence happens to be a sequence of values that match the values of the ASCII table, it won't add the '\0'-terminating byte in your read buffer. And that's a problem, because you are using functions that expect valid strings. If the read input does not contain a newline, strtok will continue reading until it finds '\0' and if that byte is not present, it will read beyond the limits. This is undefined behaviour.

执行此操作的第二个问题是read不在乎 您准备好的字节,您没有读取行,您准备了1000字节 可能包含或可能不包含字符串的内存块.最有可能的是 该块不包含字符串,因为/dev/ttyACM2会生成一个 无休止的流,并且永远不会向用户发送'\0'.

The second problem with doing this is that once again read doesn't care about the bytes you are ready, you are not reading lines, you are ready a 1000 byte block of memory which may or might not contain strings. It is most likely that the block does not contain strings, because /dev/ttyACM2 will generate an endless stream and never send '\0' to the user.

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

I would use fgets to get a line and parse it, after that get another line, and so on. Because you've only got the file descriptor, you should use:

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()的结果 到共享内存对象的状态是不确定的.

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调用.

In this version you wouldn't even need strtok_r because you wouldn't need to nest strtok calls.

修改

我之前想念的一件事是

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),因为在main中,end_of_loop 变量永远不会更改,因此永远没有必要检查该值.

Depending in the optimazations of your compiler, you will end up in an endless loop, even after pressing Ctrl+C. The compiler might optimize the while loop to while(1), because in main the end_of_loop variable is never altered, so there is no point to always check for that value.

在尝试停止带有捕获信号的循环时,最好至少声明 变量为volatile,以便编译器不会优化该变量 离开.通常(请参见 1

When trying to stop loops with catching signals, it's best to at least declare the variable as volatile, so that the compiler does not optimize that variable away. Mostly (see 1, 2) the best way to do this is:

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

这篇关于来自Linux串行端口的C Gps nmea解析器无法解析读取缓冲区的最后一行的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆