news 2026/9/14 7:19:54

IoT-For-Beginners 实战:在单板上用 pynmea2 解码 NMEA 报文,从 GPS 原始数据中提取经纬度

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
IoT-For-Beginners 实战:在单板上用 pynmea2 解码 NMEA 报文,从 GPS 原始数据中提取经纬度

IoT-For-Beginners 实战:在单板上用 pynmea2 解码 NMEA 报文,从 GPS 原始数据中提取经纬度

【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners

本文围绕 IoT-For-Beginners 课程「第 3 周 · 第 1 课:Location tracking(位置跟踪)」中的「Decode GPS data(解码 GPS 数据)」环节展开,面向 Raspberry Pi 与 CounterFit 虚拟 IoT 设备两种运行环境,完整讲解如何使用开源库pynmea2解析 UART 串口读出的 NMEA 报文,把(d)ddmm.mmmm格式的大地坐标换算为十进制度经纬度。读完本文,你可以独立完成 GPS 传感器应用的解码改造:安装依赖、改写数据处理函数、处理南北纬/东西经的符号方向,并验证解码后的坐标输出。

1. 这个环节在课程中的位置

在 3-transport/lessons/1-location-tracking 课程中,位置跟踪被拆成递进的三步,本文对应其中最后一步「解码」:

  1. 读取原始数据:按 pi-gps-sensor.md(Raspberry Pi)或 virtual-device-gps-sensor.md(虚拟设备)把 GPS 传感器接入设备,通过串口(/dev/ttyAMA0)读出 NMEA 原始报文。
  2. 理解 NMEA 报文:原始输出形如$GNGGA,020604.001,4738.538654,N,12208.341758,W,1,3,,164.7,M,-17.1,M,,*67,遵循 NMEA 0183 标准的文本格式。
  3. 解码为可用坐标(本文主题):用 NMEA 解析库把报文转换成纬度,经度的十进制度坐标,并顺带拿到参与定位的卫星数量。

课程原文(英文)位于 single-board-computer-gps-decode.md,本文在完整继承其操作步骤的基础上,结合仓库中的成品代码做了源码级补充。

2. 前置回顾:上一步读到的原始 NMEA 输出

在进入解码之前,上一步(code-gps 阶段)的程序只是把串口里的每一行原样打印出来,例如运行code-gps/pi/gps-sensor/app.py后控制台会看到:

$GNGGA,020604.001,4738.538654,N,12208.341758,W,1,3,,164.7,M,-17.1,M,,*67 $GPGSA,A,1,,,,,,,,,,,,,,,*1E $BDGSA,A,1,,,,,,,,,,,,,,,*0F $GPGSV,1,1,00*79 $BDGSV,1,1,00*68

可以对照 code-gps/pi/gps-sensor/app.py 确认:该阶段的print_gps_data函数体只有一句print(line.rstrip())。NMEA 报文里其实每条都有含义——以$开头的是句子头,后接 2 个字符表示来源(GP为美国 GPS 系统,GN为 GLONASS 等)、3 个字符表示报文类型,其余是逗号分隔的字段。本课用到的关键报文类型见课程 README 中的表格:

类型说明
GGAGPS 定位信息,包含经纬度、海拔、以及计算该定位所用的可视卫星数
ZDA当前日期时间(含本地时区)
GSV可视卫星明细(传感器能收到信号的卫星)

因此解码程序只需关心GGA句子即可拿到坐标。

3. 实操:用 pynmea2 解码 GPS 数据

以下四步完整继承自课程文档,可直接复现。

3.1 打开项目并安装解析库

  1. 如果gps-sensor应用项目尚未打开,先打开它。

  2. 安装负责 NMEA 解码的 Pip 包pynmea2

    pip3 install pynmea2

3.2 导入模块

app.py的导入区加入:

import pynmea2

3.3 替换数据处理函数

print_gps_data函数体替换为:

msg = pynmea2.parse(line) if msg.sentence_type == 'GGA': lat = pynmea2.dm_to_sd(msg.lat) lon = pynmea2.dm_to_sd(msg.lon) if msg.lat_dir == 'S': lat = lat * -1 if msg.lon_dir == 'W': lon = lon * -1 print(f'{lat},{lon} - from {msg.num_sats} satellites')

这段代码的逐段原理(与课程文档的说明一致):

  • pynmea2.parse(line):把从 UART 串口读出的这一行文本解析成结构化的 NMEA 消息对象,msg.sentence_type给出报文类型(如GGAGSV)。
  • 只有GGA是定位信息句子,其他句子(GSA、GSV 等)直接跳过。
  • GGA 中的经纬度使用 NMEA 的(d)ddmm.mmmm格式(度 + 分.分的小数),pynmea2.dm_to_sd(...)负责把该格式转换为十进制度。以课程示例为例:4738.538654,N换算后是47.642310912208.341758,W换算后是122.1390293
  • 方向字符决定正负号:纬度方向为S(南纬)时取负;经度方向为W(西经)时取负。北纬/东经保持正数。
  • 最后把坐标和msg.num_sats(本次定位使用的卫星数)一起打印到控制台。

3.4 运行并验证输出

  • 实体 Pi 用户:确保 Grove GPS 传感器已接好、天线朝向天空(接线和/boot/config.txt/boot/cmdline.txt的蓝牙串口冲突处理见 pi-gps-sensor.md)。
  • 虚拟设备用户:必须保证 CounterFit 应用正在运行并向虚拟 GPS 传感器发送数据。虚拟传感器支持三种数据源——Lat/Lon 直接给定经纬度、手动粘贴 NMEA 句子、或上传 GPX 轨迹文件;配置界面示例如下:

运行后期望看到类似输出:

pi@raspberrypi:~/gps-sensor $ python3 app.py 47.6423109,-122.1390293 - from 3 satellites

4. 仓库源码佐证:两个平台的完整成品实现

课程文档提示成品代码位于code-gps-decode目录下。把仓库中的两个版本对照阅读,可以补充文档未细说的若干工程细节。

4.1 Raspberry Pi 版本

3-transport/lessons/1-location-tracking/code-gps-decode/pi/gps-sensor/app.py 的完整结构:

import time import serial import pynmea2 import json serial = serial.Serial('/dev/ttyAMA0', 9600, timeout=1) serial.reset_input_buffer() serial.flush() def print_gps_data(line): msg = pynmea2.parse(line) if msg.sentence_type == 'GGA': ... print(f'{lat},{lon} - from {msg.num_sats} satellites') while True: try: line = serial.readline().decode('utf-8') while len(line) > 0: print_gps_data(line) line = serial.readline().decode('utf-8') except UnicodeDecodeError: line = serial.readline().decode('utf-8') time.sleep(1)

从源码结构看,有两处文档之外的健壮性设计值得注意:

  1. 串口初始化参数serial.Serial('/dev/ttyAMA0', 9600, timeout=1)使用 9600 波特率(Grove GPS 模块的默认速率)、1 秒读超时,并紧接着reset_input_buffer()flush()清空历史缓存,避免解码到开机残留的半句报文。
  2. UnicodeDecodeError兜底:串口流可能从多字节字符中间处断开读出,导致 utf-8 解码失败;Pi 版文档专门给出了该报错('utf-8' codec can't decode byte 0x93...)及try/except处理方案,成品代码中正是按此实现的——解码失败时再读一行完整数据继续。

4.2 虚拟设备版本

3-transport/lessons/1-location-tracking/code-gps-decode/virtual-device/gps-sensor/app.py 与 Pi 版的差异集中在“串口”一层:

from counterfit_connection import CounterFitConnection CounterFitConnection.init('127.0.0.1', 5000) import counterfit_shims_serial serial = counterfit_shims_serial.Serial('/dev/ttyAMA0')
  • 通过CounterFitConnection.init('127.0.0.1', 5000)连接到本机 5000 端口的 CounterFit 仿真服务,用counterfit_shims_serial垫片替代pyserial,应用层读法完全不变(同样调用serial.readline().decode('utf-8'))。
  • 解码逻辑(parse→ 判GGAdm_to_sd→ 方向取负 → 打印)与 Pi 版逐行一致,印证了“解码层与硬件层解耦”:同一份解析代码可以跑在真实硬件与仿真硬件上。
  • 细节上,该版本函数名为send_gps_data而非print_gps_data(见 virtual-device 版 app.py),阅读仓库代码时不必困惑。

5. NMEA 解码原理深入:GGA 句子逐字段解读

结合课程 README 对 NMEA 的讲解,把示例句子拆开看:

$GNGGA,020604.001,4738.538654,N,12208.341758,W,1,3,,164.7,M,-17.1,M,,*67
  • 4738.538654是纬度:47 度 38.538654 分。换算公式即度 + 分/60 = 47 + 38.538654/60 = 47.6423109,方向N为正。
  • 12208.341758是经度:122 度 08.341758 分,122 + 8.341758/60 = 122.1390293,方向W故取负,得-122.1390293
  • 该坐标(47.6423109,-122.1390293)即课程示例中位于华盛顿州 Redmond 的 Microsoft 园区位置,与第 3 节运行输出的47.6423109,-122.1390293 - from 3 satellites完全对应——其中的3 satellites来自msg.num_sats字段。

这解释了为什么dm_to_sd和方向判断缺一不可:NMEA 用“数值 + 单字母方向”编码半球,而计算机世界用“带符号十进制度”表示坐标,解码就是在两者之间做归一化。

6. 横向对照:Wio Terminal 用 TinyGPSPlus 解码的另一种实现

同一课里 wio-terminal-gps-decode.md 针对 Arduino 架构的 Wio Terminal 使用了 C++ 生态的 TinyGPSPlus 库。对照 code-gps-decode/wio-terminal 的 main.cpp,两种解码范式差异清晰:

  • 流式逐字符编码if (gps.encode(Serial3.read()))每次喂入一个字符,由库内部攒句;Python 版则是按“行”喂入整句再parse
  • 有效性校验:C++ 版用gps.location.isValid()判断定位有效后才输出;Python 版以sentence_type == 'GGA'作为处理入口。
  • 最终输出格式一致:47.6423109,-122.1390293 - from 3 satellites,便于跨平台验证结果一致性。

从源码结构看,两条路线的公共核心完全相同:只消费 GGA 定位信息、把度分格式归一化为十进制度、按半球方向赋予符号。

7. 常见问题与验证要点

现象原因与处理
控制台一直无输出(虚拟设备)CounterFit 未在运行或 GPS 传感器未发送数据;确认传感器已创建在/dev/ttyAMA0并勾选 Repeat,参见 virtual-device-gps-sensor.md
UnicodeDecodeError串口读到的首字节可能落在字符中间,按 4.1 节方式用try/except跳过该行重读
只打印部分报文类型属预期行为:GSA/GSV等句子不含坐标,程序刻意只处理GGA
经纬度符号与预期相反检查lat_dir/lon_dir判断:仅SW需取负

验证解码正确性的最直接方式:让 CounterFit 以 Lat/Lon 方式给定已知坐标(如 47.6423109 / -122.1390293),运行python3 app.py,确认控制台输出的十进制度坐标与给定值一致,卫星数与设置一致。

8. 延伸:课程给出的挑战题

完成本文流程后,可以继续课程 README 中的 Challenge:不依赖pynmea2等第三方库,自己实现一个 NMEA 解码器——按$拆句、逗号分字段、识别GGA类型、完成(d)ddmm.mmmm到十进制度与符号的转换。这正好把第 5 节的手算过程翻译成代码。后续作业则见同目录下的 assignment.md。

【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 7:18:40

USB转多引脚线缆:硬件调试的底层通信基石

1. 项目概述:一根线,撬动硬件开发的底层控制权 “USB to Multi-Pin Cable for Custom Hardware”——这根线的名字听起来平平无奇,但在我拆过上百块开发板、焊过几千个引脚、被设备管理器里一长串“未知设备”折磨到凌晨三点的那些年里&#…

作者头像 李华
网站建设 2026/9/14 7:17:22

企业级智能体效能管理:从黑盒工具到可度量数字员工

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 7:16:30

电力系统稳定器(PSS)与Simulink仿真实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华