来自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解析器.根据我在互联网上发现的几个示例,我得出以下结论:
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:
#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屋!