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 课程中,位置跟踪被拆成递进的三步,本文对应其中最后一步「解码」:
- 读取原始数据:按 pi-gps-sensor.md(Raspberry Pi)或 virtual-device-gps-sensor.md(虚拟设备)把 GPS 传感器接入设备,通过串口(
/dev/ttyAMA0)读出 NMEA 原始报文。 - 理解 NMEA 报文:原始输出形如
$GNGGA,020604.001,4738.538654,N,12208.341758,W,1,3,,164.7,M,-17.1,M,,*67,遵循 NMEA 0183 标准的文本格式。 - 解码为可用坐标(本文主题):用 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 中的表格:
| 类型 | 说明 |
|---|---|
| GGA | GPS 定位信息,包含经纬度、海拔、以及计算该定位所用的可视卫星数 |
| ZDA | 当前日期时间(含本地时区) |
| GSV | 可视卫星明细(传感器能收到信号的卫星) |
因此解码程序只需关心GGA句子即可拿到坐标。
3. 实操:用 pynmea2 解码 GPS 数据
以下四步完整继承自课程文档,可直接复现。
3.1 打开项目并安装解析库
如果
gps-sensor应用项目尚未打开,先打开它。安装负责 NMEA 解码的 Pip 包
pynmea2:pip3 install pynmea2
3.2 导入模块
在app.py的导入区加入:
import pynmea23.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给出报文类型(如GGA、GSV)。- 只有
GGA是定位信息句子,其他句子(GSA、GSV 等)直接跳过。 - GGA 中的经纬度使用 NMEA 的
(d)ddmm.mmmm格式(度 + 分.分的小数),pynmea2.dm_to_sd(...)负责把该格式转换为十进制度。以课程示例为例:4738.538654,N换算后是47.6423109,12208.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 satellites4. 仓库源码佐证:两个平台的完整成品实现
课程文档提示成品代码位于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)从源码结构看,有两处文档之外的健壮性设计值得注意:
- 串口初始化参数:
serial.Serial('/dev/ttyAMA0', 9600, timeout=1)使用 9600 波特率(Grove GPS 模块的默认速率)、1 秒读超时,并紧接着reset_input_buffer()与flush()清空历史缓存,避免解码到开机残留的半句报文。 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→ 判GGA→dm_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,,*674738.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判断:仅S与W需取负 |
验证解码正确性的最直接方式:让 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),仅供参考