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

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

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

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

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

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

#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\r\n",tokens);
     tokens = strtok (NULL,",");
    }
    return 0;
}

它的输出结果为:

上述代码,有一个地方,不知道大家注意到没有,第一次调用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.

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

#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\r\n",tokens);
        tokens = strtok (NULL,",");
    }

    return 0;
}

输出结果如下:

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

事实证明我的想法是错的,错在第三次调用strok函数后的返回值,并不是我想的那样返回NULL,实际上第三次调用后,返回值是”Potato”。也就说当检索到两个连续的逗号之间没有字符串,它会自动往后检索,把后面的下一个逗号前的字符串返回。

strtok熟悉后,我们需要思考一个重要的问题,就是如何判断出逗号间为空的状况。不然直接使用strtok循环的去解析,当出现逗号间为空时,就会出现字段无法再一一对应的情况。什么意思呢,看上面的代码,就是程序并没法知道第三个字段是空,解析出来的”Potato”也不知道对应是第几个字段的。

可以考虑采用以下方式来解决,程序里先去判断是否有连续逗号(",,"),如果有则将",,"替换为",@,"形式,其中@是一个正常情况下该字段不会出现的字符。这样操作之后逗号分隔的各个字段就都有了内容,再进行解析就不会出现上述的问题了。那如何用程序实现字符串的替换功能呢?

即对于上述字符串:"Apple,Pear,,Potato,11"

我们希望经过替换后字符串变为:

"Apple,Pear,@,Potato,11"

大家可以看一下下面的代码(替换函数strrpl是直接谷哥出来的)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char* strrpl(char *str, char* find, char *replace)
{
    int i;
    char *pt = strstr(str, find);
    char *firstStr;
    if(pt == NULL){
        printf("cannot find string \n");
        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\n",str);
    
    char *tokens = strtok (str,",");
        
    //iterate over tokens.. .
    while (tokens!= NULL)
    {
        printf ("%s\r\n",tokens);
        tokens = strtok (NULL,",");
    }

    return 0;
}

输出的结果是:

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

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

运行一下,我们看看结果

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char* strrpl(char *str, char* find, char *replace)
{
    int i;
    char *pt = strstr(str, find);
    char *firstStr;
    if(pt == NULL){
        printf("cannot find string \n");
        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\n",str);
    
    char *tokens = strtok (str,",");
        
    //iterate over tokens.. .
    while (tokens!= NULL)
    {
        printf ("%s\r\n",tokens);
        tokens = strtok (NULL,",");
    }

    return 0;
}

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

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

改进后的代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char* strrpl(char *str, char* find, char *replace)
{
    int i;
    char *pt = strstr(str, find);
    char *firstStr;
    if(pt == NULL){
        printf("cannot find string \n");
        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, 0, sizeof(str)+100);
    memcpy(buff, str, sizeof(str));

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

    printf("%s\n",buff);
    
    char *tokens = strtok (buff,",");
        
    //iterate over tokens.. .
    while (tokens!= NULL)
    {
        printf ("%s\r\n",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\n\
$GNVTG,,T,,M,0.253,N,0.468,K,D*36\n\
$GNGGA,051035.00,4000.74054,N,11628.03344,E,2,08,2.08,3.3,M,-8.3,M,,0000*5D\n\
$GNGSA,A,3,29,14,27,42,03,,,,,,,,3.33,2.08,2.60*1F\n\
$GNGSA,A,3,87,66,67,,,,,,,,,,3.33,2.08,2.60*1F\n\
$GPGSV,5,1,17,03,15,250,28,04,47,302,17,08,03,196,09,09,16,318,13*7B\n\
$GPGSV,5,2,17,14,23,157,32,16,72,264,19,21,08,092,20,22,07,230,34*77\n\
$GPGSV,5,3,17,23,41,303,,26,72,027,21,27,29,179,28,29,15,039,30*77\n\
$GPGSV,5,4,17,31,47,089,15,40,13,251,,41,32,226,31,42,35,140,31*7D\n\
$GPGSV,5,5,17,50,42,164,34*48\n\
$GLGSV,3,1,10,66,12,192,26,67,44,240,28,68,34,310,,76,25,063,*6E\n\
$GLGSV,3,2,10,77,58,357,,78,29,287,,85,01,012,,86,30,057,*60\n\
$GLGSV,3,3,10,87,26,128,32,88,00,163,*61\n\
$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个问题需要做对应的修改,改后的代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char* strrpl(char *str, char* find, char *replace)
{
    int i;
    char *pt = strstr(str, find);   
    char *firstStr;
   
    if(pt == NULL){
        printf("cannot find string \n");
        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\n\
$GNVTG,,T,,M,0.253,N,0.468,K,D*36\n\
$GNGGA,051035.00,4000.74054,N,11628.03344,E,2,08,2.08,3.3,M,-8.3,M,,0000*5D\n\
$GNGSA,A,3,29,14,27,42,03,,,,,,,,3.33,2.08,2.60*1F\n\
$GNGSA,A,3,87,66,67,,,,,,,,,,3.33,2.08,2.60*1F\n\
$GPGSV,5,1,17,03,15,250,28,04,47,302,17,08,03,196,09,09,16,318,13*7B\n\
$GPGSV,5,2,17,14,23,157,32,16,72,264,19,21,08,092,20,22,07,230,34*77\n\
$GPGSV,5,3,17,23,41,303,,26,72,027,21,27,29,179,28,29,15,039,30*77\n\
$GPGSV,5,4,17,31,47,089,15,40,13,251,,41,32,226,31,42,35,140,31*7D\n\
$GPGSV,5,5,17,50,42,164,34*48\n\
$GLGSV,3,1,10,66,12,192,26,67,44,240,28,68,34,310,,76,25,063,*6E\n\
$GLGSV,3,2,10,77,58,357,,78,29,287,,85,01,012,,86,30,057,*60\n\
$GLGSV,3,3,10,87,26,128,32,88,00,163,*61\n\
$GNGLL,4000.74054,N,11628.03344,E,051035.00,A,D*7A";
    
    char *buff;

    buff = malloc(sizeof(str)+100);
    memset(buff, 0, sizeof(str)+100);
    memcpy(buff, str, sizeof(str));

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

    printf("%s\n",buff);
    
    char *tokens = strtok (buff,",");
        
    //iterate over tokens.. .
    while (tokens!= NULL)
    {
        printf ("%s\r\n",tokens);
        tokens = strtok (NULL,",");
    }

    free(buff);

    return 0;
}

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

有了以上基础,就可以实际来写GPS数据解析的代码了,在上一篇文章的基础上,我对整个目录结构做了调整,新的工程总共有5个文件,mian.c为主程序,gnss.c和gnss.h和GNSS数据解析相关,uart.c和uart.h对应串口配置。

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

上述代码中重点是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 */
    if( NULL==(ptr=strstr(buff,"$GPRMC")) && NULL==(ptr=strstr(buff,"$GNRMC")) )
    {
        return -2;
    }

    if(check_nmea_message(ptr, 0, buff_len) <0 )
    {
        printf("check error!\n");
        return -3;
    }
    
    char *tmpbuf;
    tmpbuf = (char *)malloc(strlen(ptr)+100);
    memset(tmpbuf, 0, strlen(ptr)+100);
    memcpy(tmpbuf, ptr, strlen(ptr));
    
    while (strstr(tmpbuf, ",,"))
        strrpl(tmpbuf, ",,", ",@,");
    
    printf("tmpbuf:%s \n",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的原因是你分配的大小要能能容纳字符串(尾部以'\0'结尾),而strlen(str)的长度不包含尾部的'\0'

3) 要养成初始化指针、内存空间后,立刻赋初值的习惯。

4)strok函数适合用来分割字符串,解析各个字段。

5)操作字符串/字符数组时一定要注意越界的问题。

完整工程见:

https://download.csdn.net/download/wangwenxue1989/12208715 

欢迎扫码关注我们:

 

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值