分享

Linux项目实战系列之:GPS数据解析

 TopSemic嵌入式 2023-08-12 发布于北京

    在之前一篇文章:嵌入式Linux系列第21篇:应用程序之开篇闲聊 里,当时给自己定了一个小目标,要实现如下功能的小项目:

    1) 串口1实时读取GPS数据,同时转发到串口2输出

    2) 将获取到的经纬度信息,通过网口UDP方式发送到电脑端,电脑端通过上位机软件实时显示设备的位置信息。

    3) 安卓手机可以通过WIFI连接到板子,手机APP也可以显示设备的位置信息。

    4) 设备通过4G将位置信息传输到云平台,在任何一个可以上网的电脑上通过浏览器可以实时显示设备的位置信息。

    今天这篇文章要完成的功能是串口读取并解析GPS数据。

    GPS数据解析的核心问题可以归结为如何解析以逗号作为分隔符的字符串问题。看似很简单的一个功能,真正实现起来也那不是那么容易,在调试的过程中,我就遇到了很多的小问题,在此做个完整的记录与总结,希望对大家有帮助。

首先给大家介绍一下strtok函数,它是标准函数库中的一员,标准函数库是一个工具箱,它能极大地扩展C程序员的能力,我们需要熟悉并且灵活的应用

char *strtok(char *str, const char *delim)功能是分解字符串str 为一组字符串,delim为分隔符。

该函数返回被分解的第一个子字符串,如果没有可检索的字符串,则返回一个空指针。

我们看一下这个函数的使用例子,

 程序1: strtok函数使用示例1

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

int main(void)

{

    char str[] ="Apple,Pear,Potato,11";

    char* tokens = strtok (str,",");

    //iterate over tokens.. .

    while (tokens!= NULL)

    {

     printf ("%s",tokens);

     tokens = strtok (NULL,",");

    }

    return 0;

}

它的输出结果为:

Apple

Pear

Potato

11

上述代码,有一个地方,不知道大家注意到没有,第一次调用strtok的时候,第一个参数为str,后面每次调用时参数都是NULL。The first call to strtok must pass the C string to tokenize, and subsequent calls must specify NULL as the first argument, which tells the function to continue tokenizing the string you passed in first.

如果逗号之间为空,情况会是什么样子呢?看一下下面的例子:

程序清2: strtok函数使用示例2

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

int main(void)

{

    char str[] ="Apple,Pear,,Potato,11";

    char *tokens = strtok (str,",");

    //iterate over tokens.. .

    while (tokens!= NULL)

    {

        printf ("%s",tokens);

        tokens = strtok (NULL,",");

    }

    return 0;

}

输出结果如下:

Apple

Pear

Potato

11

和第一个程序输出的结果完全一致,起初我对这个结果很不理解,我本能的以为第一次调用strtok的返回值是”Apple”,第二次调用strtok的返回值为”Pear”,第三次调用后,由于2个逗号之间是空的,我以为返回值会是NULL,然后在第四次调用后,得到”Potato”。

事实证明我的想法是错的,错在第三次调用strok函数后的返回值,并不是我想的那样返回NULL,实际上第三次调用后,返回值是”Potato”。也就说当检索到两个连续的逗号之间没有字符串,它会自动往后检索,把后面的下一个逗号前的字符串返回。
strtok熟悉后,我们需要思考一个重要的问题,就是如何判断出逗号间为空的状况。不然直接使用strtok循环的去解析,当出现逗号间为空时,就会出现字段无法再一一对应的情况。什么意思呢,看上面的代码,就是程序并没法知道第三个字段是空,解析出来的”Potato”也不知道对应是第几个字段的。
可以考虑采用以下方式来解决,程序里先去判断是否有连续逗号(",,"),如果有则将",,"替换为",@,"形式,其中@是一个正常情况下该字段不会出现的字符。这样操作之后逗号分隔的各个字段就都有了内容,再进行解析就不会出现上述的问题了。那如何用程序实现字符串的替换功能呢?
即对于上述字符串"Apple,Pear,,Potato,11"
我们希望经过替换后字符串变为:
"Apple,Pear,@,Potato,11"
大家可以看一下下面的代码(替换函数strrpl是直接谷哥出来的)
程序清3:实现字符串替换功能

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

charstrrpl(char *strcharfindchar *replace)

{

    int i;

    char *pt = strstr(str, find);

    char *firstStr;

    if(pt == NULL){

        printf("cannot find string ");

        return NULL;

    }

    firstStr = (char* )malloc(100 * sizeof(char));

    // copy just until i find what i need to replace

    // i tried to specify the length of firstStr just with pt - str

    strncpy(firstStr, str, strlen(str) - strlen(pt)); 

    strcat(firstStr, replace);

    strcat(firstStr, pt + strlen(find));

    for(i = 0; i < strlen(firstStr); i++)

        str[i] = firstStr[i];

    return str;

}

int main(void)

{

    char str[] ="Apple,Pear,,Potato,11";

    strrpl(str,",,",",@,");

    printf ("%s",str);

    char *tokens = strtok (str,",");

    //iterate over tokens.. .

    while (tokens!= NULL)

    {

        printf ("%s",tokens);

        tokens = strtok (NULL,",");

    }

    return 0;

}

输出的结果是:

这样就实现了两个逗号替换的功能,如果字符串是下面这个呢? 该字符串中间出现了连续3个逗号,并且后面还有一次连续2个逗号,

char str[] ="Apple,Pear,,,Potato,,11";

运行一下,我们看看结果

结果是只替换了第一个连续逗号的地方,如何实现让字符串里所有的连续逗号都被替换呢?重复的做一件事,只需要加一个循环即可,修改后的代码如下:

程序清4:循环替换字符串功能

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

charstrrpl(char *strcharfindchar *replace)

{

    int i;

    char *pt = strstr(str, find);

    char *firstStr;

    if(pt == NULL){

        printf("cannot find string ");

        return NULL;

    }

    firstStr = (char* )malloc(100 * sizeof(char));

    // copy just until i find what i need to replace

    // i tried to specify the length of firstStr just with pt - str

    strncpy(firstStr, str, strlen(str) - strlen(pt)); 

    strcat(firstStr, replace);

    strcat(firstStr, pt + strlen(find));

    for(i = 0; i < strlen(firstStr); i++)

        str[i] = firstStr[i];

    return str;

}

int main(void)

{

    char str[] ="Apple,Pear,,,Potato,,11";

    while (strstr(str, ",,"))

        strrpl(str, ",,"",@,");

    printf("%s",str);

    char *tokens = strtok (str,",");

    //iterate over tokens.. .

    while (tokens!= NULL)

    {

        printf ("%s",tokens);

        tokens = strtok (NULL,",");

    }

    return 0;

}

   这个代码运行后出现了如下问题:

看起来像是数组越界了,经过分析可知是str数组越界导致的,由于“,,”被替换成“,@,” ,导致数组长度变长从而产生越界。所以上述代码不能那么写,我们可以通过定义一个新的更长长度的数组来解决。另外还有一点需要注意的是:strok函数执行任务时,它会修改它所处理的字符串,如果源字符串不能被修改,就必须得复制一份,将这份拷贝传给strok函数。

改进后的代码如下:

程序清5:字符串操作时要防止越界

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

charstrrpl(char *strcharfindchar *replace)

{

    int i;

    char *pt = strstr(str, find);

    char *firstStr;

    if(pt == NULL){

        printf("cannot find string ");

        return NULL;

    }

    firstStr = (char* )malloc(100 * sizeof(char));

    // copy just until i find what i need to replace

    // i tried to specify the length of firstStr just with pt - str

    strncpy(firstStr, str, strlen(str) - strlen(pt)); 

    strcat(firstStr, replace);

    strcat(firstStr, pt + strlen(find));

    for(i = 0; i < strlen(firstStr); i++)

        str[i] = firstStr[i];

    return str;

}

int main(void)

{

    char str[] ="Apple,Pear,,,Potato,,11";

    char *buff;

    buff = malloc(sizeof(str)+100);

    memset(buff, 0sizeof(str)+100);

    memcpy(buff, str, sizeof(str));

    while (strstr(buff, ",,"))

        strrpl(buff, ",,"",@,");

    printf("%s",buff);

    char *tokens = strtok (buff,",");

    //iterate over tokens.. .

    while (tokens!= NULL)

    {

        printf ("%s",tokens);

        tokens = strtok (NULL,",");

    }

    free(buff);

    return 0;

}

输出结果如下:

经过修改了的这份代码是不是就没有问题了呢?答案是否!如果我将str数组变长,变成下面的这一串内容

    char str[] = "$GNRMC,051035.00,A,4000.74054,N,11628.03344,E,0.253,,020320,6.91,W,D*23

$GNVTG,,T,,M,0.253,N,0.468,K,D*36

$GNGGA,051035.00,4000.74054,N,11628.03344,E,2,08,2.08,3.3,M,-8.3,M,,0000*5D

$GNGSA,A,3,29,14,27,42,03,,,,,,,,3.33,2.08,2.60*1F

$GNGSA,A,3,87,66,67,,,,,,,,,,3.33,2.08,2.60*1F

$GPGSV,5,1,17,03,15,250,28,04,47,302,17,08,03,196,09,09,16,318,13*7B

$GPGSV,5,2,17,14,23,157,32,16,72,264,19,21,08,092,20,22,07,230,34*77

$GPGSV,5,3,17,23,41,303,,26,72,027,21,27,29,179,28,29,15,039,30*77

$GPGSV,5,4,17,31,47,089,15,40,13,251,,41,32,226,31,42,35,140,31*7D

$GPGSV,5,5,17,50,42,164,34*48

$GLGSV,3,1,10,66,12,192,26,67,44,240,28,68,34,310,,76,25,063,*6E

$GLGSV,3,2,10,77,58,357,,78,29,287,,85,01,012,,86,30,057,*60

$GLGSV,3,3,10,87,26,128,32,88,00,163,*61

$GNGLL,4000.74054,N,11628.03344,E,051035.00,A,D*7A";

其他代码不变,运行结果是:

在出现这个问题之前,我都没有仔细的阅读直接拷贝过来strrpl函数内部实现细节,这时就得好好看看了,经过很长时间调试,找到问题出在下面这句话上面,

firstStr = (char* )malloc(100 * sizeof(char));

和这句话相关,有3个非常重要的值得大家注意的地方:

1)分配100字节显然是不合理的,firstStr是用来存放经过替换后的字符串的,所以它的长度取决于源字符串长度,以及替换和被替换的字符串长度,不能暴力的随便设置一个数。
2)在调用malloc函数后,这个空间没有赋初值,这是相当危险的。
3)在调用malloc后,没有调用free函数,会产生内存泄露。
针对以上3个问题需要做对应的修改,改后的代码如下:
程序清6:修改strrpl函数

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

charstrrpl(char *strcharfindchar *replace)

{

    int i;

    char *pt = strstr(str, find);   

    char *firstStr;

    if(pt == NULL){

        printf("cannot find string ");

        return NULL;

    }

    int len = strlen(str)+1+strlen(replace)-strlen(find);

    firstStr = (char* )malloc(len);

    memset(firstStr,0,len);

    // copy just until i find what i need to replace

    // i tried to specify the length of firstStr just with pt - str

    strncpy(firstStr, str, strlen(str) - strlen(pt)); 

    strcat(firstStr, replace);

    strcat(firstStr, pt + strlen(find));

    for(i = 0; i < strlen(firstStr); i++)

        str[i] = firstStr[i];

    free(firstStr);

    return str;

}

int main(void)

{

    char str[] = "$GNRMC,051035.00,A,4000.74054,N,11628.03344,E,0.253,,020320,6.91,W,D*23

$GNVTG,,T,,M,0.253,N,0.468,K,D*36

$GNGGA,051035.00,4000.74054,N,11628.03344,E,2,08,2.08,3.3,M,-8.3,M,,0000*5D

$GNGSA,A,3,29,14,27,42,03,,,,,,,,3.33,2.08,2.60*1F

$GNGSA,A,3,87,66,67,,,,,,,,,,3.33,2.08,2.60*1F

$GPGSV,5,1,17,03,15,250,28,04,47,302,17,08,03,196,09,09,16,318,13*7B

$GPGSV,5,2,17,14,23,157,32,16,72,264,19,21,08,092,20,22,07,230,34*77

$GPGSV,5,3,17,23,41,303,,26,72,027,21,27,29,179,28,29,15,039,30*77

$GPGSV,5,4,17,31,47,089,15,40,13,251,,41,32,226,31,42,35,140,31*7D

$GPGSV,5,5,17,50,42,164,34*48

$GLGSV,3,1,10,66,12,192,26,67,44,240,28,68,34,310,,76,25,063,*6E

$GLGSV,3,2,10,77,58,357,,78,29,287,,85,01,012,,86,30,057,*60

$GLGSV,3,3,10,87,26,128,32,88,00,163,*61

$GNGLL,4000.74054,N,11628.03344,E,051035.00,A,D*7A";

    char *buff;

    buff = malloc(sizeof(str)+100);

    memset(buff, 0sizeof(str)+100);

    memcpy(buff, str, sizeof(str));

    while (strstr(buff, ",,"))

        strrpl(buff, ",,"",@,");

    printf("%s",buff);

    char *tokens = strtok (buff,",");

    //iterate over tokens.. .

    while (tokens!= NULL)

    {

        printf ("%s",tokens);

        tokens = strtok (NULL,",");

    }

    free(buff);

    return 0;

}

这样再次运行代码,就可以得到正确的结果了。

有了以上基础,就可以实际来写GPS数据解析的代码了,整个的工程目录总共有6个文件,mian.c为主程序,gnss.c和gnss.h和GNSS数据解析相关,uart.c和uart.h对应串口配置,还有1个Makefile文件。

运行后,会输出如下信息:

上述代码中重点是gnss.c文件中的gps_analyse函数,大家可以好好看看,

int gps_analyse(char *buff,int buff_len,GNSS *gps_data)

{

    char *ptr = NULL;

    if(strlen(buff)<10)

    {

        return -1;

    }

    /* 如果buff字符串中包含字符"$GPRMC"则将$GPRMC的地址赋值给ptr */

    ifNULL==(ptr=strstr(buff,"$GPRMC")) && NULL==(ptr=strstr(buff,"$GNRMC")) )

    {

        return -2;

    }

    if(check_nmea_message(ptr, 0, buff_len) <0 )

    {

        printf("check error!");

        return -3;

    }

    char *tmpbuf;

    tmpbuf = (char *)malloc(strlen(ptr)+100);

    memset(tmpbuf, 0strlen(ptr)+100);

    memcpy(tmpbuf, ptr, strlen(ptr));

    while (strstr(tmpbuf, ",,"))

        strrpl(tmpbuf, ",,"",@,");

    printf("tmpbuf:%s ",tmpbuf);

    char* pch = strtok(tmpbuf, ",");

    // 1 time

    pch = strtok(NULL",");

    nmea_get_time(pch, &gps_data->time);

    // 2 status

    pch = strtok(NULL",");

    gps_data->pos_state = *pch;

    //3 latitude

    pch = strtok(NULL",");

    nmea_lat_long_to_double(&gps_data->latitude, pch, strlen(pch));

    //4 latitude direction

    pch = strtok(NULL",");

    gps_data->NS = *pch;

    //5 longitude

    pch = strtok(NULL",");

    nmea_lat_long_to_double(&gps_data->longitude, pch, strlen(pch));

    //6 long direct

    pch = strtok(NULL",");

    gps_data->EW = *pch;

    //7 speed

    pch = strtok(NULL",");

    gps_data->speed = 1.852 * strtof(pch, (char **) NULL ) / 3.6;

    //8 direction

    pch = strtok(NULL",");

    gps_data->direction = strtof(pch, (char**)NULL);

    //9 date

    pch = strtok(NULL",");

    nmea_get_date(pch, &gps_data->time);

    //10 不处理

    pch = strtok(NULL",");

    //11 不处理

    pch = strtok(NULL",");

    //12 mode

    pch = strtok(NULL",");

    gps_data->pos_mode = *pch;

    free(tmpbuf);

    return 0;

}

我在调试过程中遇到了很多的问题,通过自己实际动手搬运、修改、调试代码收获了很多知识,主要有以下几点:

1) 在使用strtof、strtod函数时,一定要加上头文件#include <stdlib.h>,否则虽然能编译通过(有警告),但是转换后的结果不对。另外一定要养成不放过编译过程中任何一个警告的习惯。
2) strrpl函数中,malloc分配的空间大小一定要注意,我一开始因为少加了个1,导致程序出现异常,调试了很久才找到问题。加1的原因是你分配的大小要能能容纳字符串(尾部以''结尾),而strlen(str)的长度不包含尾部的''。
3) 要养成初始化指针、内存空间后,立刻赋初值的习惯。
4) strok函数适合用来分割字符串,解析各个字段。
5) 操作字符串/字符数组时一定要注意越界的问题。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多