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;
}
}
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
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|
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.
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 lineyou get
If you change it to
you'll most probably see
A second problem is that you are using
strtok
in a reentrant kind of way. At the beginning of the loop, you donmea_line = strtok(read_buffer, "\n");
. Then you go to parse the line and enter a new loop. Then you do stuff linechar *token = strtok(nmea_line, ",");
and by doing thisstrtok
forgets the information about the first call.At the end of everything you do again
nmea_line = strtok(NULL, "\n");
, but thisNULL
applies to whichstrtok
? Depending on the output you will never know but it won't certainly match to with tonmea_line = strtok(read_buffer, "\n");
.Luckly there is a reentrant version of
strtok
:strtok_r
Example:
Output:
Another problem that I see is this:
The
read
function, unlikefgets
, does not read strings, it reads bytes. That means thatread
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.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.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:So I would do:
In this version you wouldn't even need
strtok_r
because you wouldn't need to neststrtok
calls.edit
There is one thing that I've missed earlier:
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 towhile(1)
, because inmain
theend_of_loop
variable is never altered, so there is no point to always check for that value.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: