来自 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
获取一行并解析它,然后再获取另一行,然后
很快。因为你只有文件描述符,所以你应该使用:
#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)
,因为在 main
中 end_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);
}
}
我需要为我正在处理的电路板(带有 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 tostrtok()
, the string to be parsed should be specified instr
. In each subsequent call that should parse the same string,str
must beNULL
.[...]
The
strtok_r()
function is a reentrant versionstrtok()
. The saveptr argument is a pointer to achar*
variable that is used internally bystrtok_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
获取一行并解析它,然后再获取另一行,然后
很快。因为你只有文件描述符,所以你应该使用:
#include <stdio.h> FILE *fdopen(int fd, const char *mode);
The
fdopen()
function associates a stream with the existing file descriptor,fd
. Themode
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 tofd
, 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 byfdopen()
is closed. The result of applyingfdopen()
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)
,因为在 main
中 end_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);
}
}