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

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

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

相关文章
|
11月前
|
Unix Linux
对于Linux的进程概念以及进程状态的理解和解析
现在,我们已经了解了Linux进程的基础知识和进程状态的理解了。这就像我们理解了城市中行人的行走和行为模式!希望这个形象的例子能帮助我们更好地理解这个重要的概念,并在实际应用中发挥作用。
213 20
|
11月前
|
Ubuntu Linux
"unzip"命令解析:Linux下如何处理压缩文件。
总的来说,`unzip`命令是Linux系统下一款实用而方便的ZIP格式文件处理工具。本文通过简明扼要的方式,详细介绍了在各类Linux发行版上安装 `unzip`的方法,以及如何使用 `unzip`命令进行解压、查看和测试ZIP文件。希望本文章能为用户带来实际帮助,提高日常操作的效率。
2478 12
|
12月前
|
关系型数据库 MySQL Linux
在Linux环境下备份Docker中的MySQL数据并传输到其他服务器以实现数据级别的容灾
以上就是在Linux环境下备份Docker中的MySQL数据并传输到其他服务器以实现数据级别的容灾的步骤。这个过程就像是一场接力赛,数据从MySQL数据库中接力棒一样传递到备份文件,再从备份文件传递到其他服务器,最后再传递回MySQL数据库。这样,即使在灾难发生时,我们也可以快速恢复数据,保证业务的正常运行。
518 28
|
12月前
|
Linux
Linux命令的基本格式解析
总的来说,Linux命令的基本格式就像一个食谱,它可以指导你如何使用你的计算机。通过学习和实践,你可以成为一个真正的“计算机厨师”,创造出各种“美味”的命令。
303 15
|
12月前
|
存储 Linux
Linux内核中的current机制解析
总的来说,current机制是Linux内核中进程管理的基础,它通过获取当前进程的task_struct结构的地址,可以方便地获取和修改进程的信息。这个机制在内核中的使用非常广泛,对于理解Linux内核的工作原理有着重要的意义。
534 11
|
数据采集 JSON 数据可视化
JSON数据解析实战:从嵌套结构到结构化表格
在信息爆炸的时代,从杂乱数据中提取精准知识图谱是数据侦探的挑战。本文以Google Scholar为例,解析嵌套JSON数据,提取文献信息并转换为结构化表格,通过Graphviz制作技术关系图谱,揭示文献间的隐秘联系。代码涵盖代理IP、请求头设置、JSON解析及可视化,提供完整实战案例。
756 4
JSON数据解析实战:从嵌套结构到结构化表格
|
监控 Shell Linux
Android调试终极指南:ADB安装+多设备连接+ANR日志抓取全流程解析,覆盖环境变量配置/多设备调试/ANR日志分析全流程,附Win/Mac/Linux三平台解决方案
ADB(Android Debug Bridge)是安卓开发中的重要工具,用于连接电脑与安卓设备,实现文件传输、应用管理、日志抓取等功能。本文介绍了 ADB 的基本概念、安装配置及常用命令。包括:1) 基本命令如 `adb version` 和 `adb devices`;2) 权限操作如 `adb root` 和 `adb shell`;3) APK 操作如安装、卸载应用;4) 文件传输如 `adb push` 和 `adb pull`;5) 日志记录如 `adb logcat`;6) 系统信息获取如屏幕截图和录屏。通过这些功能,用户可高效调试和管理安卓设备。
|
JSON 监控 网络协议
Bilibili直播信息流:连接方法与数据解析
本文详细介绍了自行实现B站直播WebSocket连接的完整流程。解析了基于WebSocket的应用层协议结构,涵盖认证包构建、心跳机制维护及数据包解析步骤,为开发者定制直播数据监控提供了完整技术方案。
1690 9
|
机器学习/深度学习 JSON 算法
淘宝拍立淘按图搜索API接口系列的应用与数据解析
淘宝拍立淘按图搜索API接口是阿里巴巴旗下淘宝平台提供的一项基于图像识别技术的创新服务。以下是对该接口系列的应用与数据解析的详细分析
|
缓存 监控 搜索推荐
【实战解析】smallredbook.item_get_video API:小红书视频数据获取与电商应用指南
本文介绍小红书官方API——`smallredbook.item_get_video`的功能与使用方法。该接口可获取笔记视频详情,包括无水印直链、封面图、时长、文本描述、标签及互动数据等,并支持电商场景分析。调用需提供`key`、`secret`和`num_iid`参数,返回字段涵盖视频链接、标题、标签及用户信息等。同时,文章提供了电商实战技巧,如竞品监控与个性化推荐,并列出合规注意事项及替代方案对比。最后解答了常见问题,如笔记ID获取与视频链接时效性等。

热门文章

最新文章