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

相关文章
|
10天前
|
自然语言处理 数据可视化 数据挖掘
带你飞上云端,轻松解析数据——gopup库详细解析--包含安装库教程
本文介绍了Python库gopup,它是一个用于轻松爬取互联网数据的工具,简化了数据收集和处理的过程。文章提供了gopup的安装教程,特别强调了安装时需注意setuptools版本,并给出了PyCharm和命令行两种安装方法。gopup库能获取包括指数、百度和谷歌数据等多种类型的数据。文中还展示了如何使用gopup获取微博指数和豆瓣新片榜数据,并通过代码示例呈现数据和图表。此外,文章提醒了使用时的风险和部分接口的失效情况,并提供了库文档链接以供深入学习。gopup库适用于数据可视化和数据分析,是进行文本挖掘和自然语言处理项目的理想工具。
46 0
带你飞上云端,轻松解析数据——gopup库详细解析--包含安装库教程
|
6天前
|
Cloud Native Linux 开发者
【Docker】Docker:解析容器化技术的利器与在Linux中的关键作用
【Docker】Docker:解析容器化技术的利器与在Linux中的关键作用
|
2天前
|
Linux Shell
【Linux】深度解析Linux中的几种进程状态
【Linux】深度解析Linux中的几种进程状态
|
3天前
|
算法 安全 Linux
深度解析:Linux内核内存管理机制
【4月更文挑战第30天】 在操作系统领域,内存管理是核心功能之一,尤其对于多任务操作系统来说更是如此。本文将深入探讨Linux操作系统的内核内存管理机制,包括物理内存的分配与回收、虚拟内存的映射以及页面替换算法等关键技术。通过对这些技术的详细剖析,我们不仅能够理解操作系统如何高效地利用有限的硬件资源,还能领会到系统设计中的性能与复杂度之间的权衡。
|
5天前
|
数据采集 存储 大数据
Python爬虫:数据获取与解析的艺术
本文介绍了Python爬虫在大数据时代的作用,重点讲解了Python爬虫基础、常用库及实战案例。Python因其简洁语法和丰富库支持成为爬虫开发的优选语言。文中提到了requests(发送HTTP请求)、BeautifulSoup(解析HTML)、Scrapy(爬虫框架)、Selenium(处理动态网页)和pandas(数据处理分析)等关键库。实战案例展示了如何爬取电商网站的商品信息,包括确定目标、发送请求、解析内容、存储数据、遍历多页及数据处理。最后,文章强调了遵守网站规则和尊重隐私的重要性。
16 2
|
7天前
|
移动开发 数据可视化 Linux
Linux 中的文件与目录管理解析
当谈到Linux系统,文件与目录管理是其中最基本和重要的部分之一。Linux提供了一种强大而灵活的方式来组织和管理文件和目录,让用户能够轻松地访问和操作系统中的各种数据。上一节我们说到文件的属性,本文将详细介绍Linux中的文件与目录管理的各个方面。
|
7天前
|
Linux Go 数据安全/隐私保护
Linux 中的文件属性解析
在 Linux 系统中,每个文件和目录有一组属性控制其操作和访问权限。了解这些属性对有效管理文件至关重要。文件属性包括:文件类型(如 `-` 表示普通文件,`d` 表示目录),权限(如 `rwx` 表示所有者权限,`r-x` 表示组和其他用户权限),所有者,组,硬链接数,文件大小和最后修改时间。通过 `chown` 和 `chmod` 命令可更改文件所有者、所属组及权限。此外,还有特殊权限(如 SUID、SGID)和 ACL(访问控制列表)提供更精细的访问控制。
|
7天前
|
存储 文件存储 数据库
Flutter的持久化存储:使用SharedPreferences等进行数据存储的技术解析
【4月更文挑战第26天】 Flutter应用持久化存储主要使用SharedPreferences,适合存储简单数据类型。通过shared_preferences插件,可轻松进行数据读写。注意异步操作、数据类型限制和安全性,对于复杂需求,可选择SQLite或文件存储。
|
8天前
|
存储 编译器 C语言
C语言基础知识:数据在内存中的存储解析(整数,浮点数)
C语言基础知识:数据在内存中的存储解析(整数,浮点数)
|
9天前
|
SQL 关系型数据库 API
从API获取数据并将其插入到PostgreSQL数据库:步骤解析
使用Python处理从API获取的数据并插入到PostgreSQL数据库:安装`psycopg2`,建立数据库连接,确保DataFrame与表结构匹配,然后使用`to_sql`方法将数据插入到已存在的表中。注意数据准备、权限设置、性能优化和安全处理。

热门文章

最新文章