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) 操作字符串/字符数组时一定要注意越界的问题。

相关文章
|
1月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
161 2
|
19天前
|
自然语言处理 数据可视化 前端开发
从数据提取到管理:合合信息的智能文档处理全方位解析【合合信息智能文档处理百宝箱】
合合信息的智能文档处理“百宝箱”涵盖文档解析、向量化模型、测评工具等,解决了复杂文档解析、大模型问答幻觉、文档解析效果评估、知识库搭建、多语言文档翻译等问题。通过可视化解析工具 TextIn ParseX、向量化模型 acge-embedding 和文档解析测评工具 markdown_tester,百宝箱提升了文档处理的效率和精确度,适用于多种文档格式和语言环境,助力企业实现高效的信息管理和业务支持。
3940 3
从数据提取到管理:合合信息的智能文档处理全方位解析【合合信息智能文档处理百宝箱】
|
10天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
38 4
|
9天前
|
存储 分布式计算 Java
存算分离与计算向数据移动:深度解析与Java实现
【11月更文挑战第10天】随着大数据时代的到来,数据量的激增给传统的数据处理架构带来了巨大的挑战。传统的“存算一体”架构,即计算资源与存储资源紧密耦合,在处理海量数据时逐渐显露出其局限性。为了应对这些挑战,存算分离(Disaggregated Storage and Compute Architecture)和计算向数据移动(Compute Moves to Data)两种架构应运而生,成为大数据处理领域的热门技术。
27 2
|
11天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
15天前
|
JavaScript API 开发工具
<大厂实战场景> ~ Flutter&鸿蒙next 解析后端返回的 HTML 数据详解
本文介绍了如何在 Flutter 中解析后端返回的 HTML 数据。首先解释了 HTML 解析的概念,然后详细介绍了使用 `http` 和 `html` 库的步骤,包括添加依赖、获取 HTML 数据、解析 HTML 内容和在 Flutter UI 中显示解析结果。通过具体的代码示例,展示了如何从 URL 获取 HTML 并提取特定信息,如链接列表。希望本文能帮助你在 Flutter 应用中更好地处理 HTML 数据。
93 1
|
1月前
|
数据采集 XML 前端开发
Jsoup在Java中:解析京东网站数据
Jsoup在Java中:解析京东网站数据
|
15天前
|
JSON 前端开发 JavaScript
API接口商品详情接口数据解析
商品详情接口通常用于提供特定商品的详细信息,这些信息比商品列表接口中的信息更加详细和全面。以下是一个示例的JSON数据格式,用于表示一个商品详情API接口的响应。这个示例假定API返回一个包含商品详细信息的对象。
|
28天前
|
JavaScript 前端开发 索引
Vue3 + Vite项目实战:常见问题与解决方案全解析
Vue3 + Vite项目实战:常见问题与解决方案全解析
44 0
|
28天前
|
API
Vue3组件通信全解析:利用props、emit、provide/inject跨层级传递数据,expose与ref实现父子组件方法调用
Vue3组件通信全解析:利用props、emit、provide/inject跨层级传递数据,expose与ref实现父子组件方法调用
352 0