IoT-For-Beginners 运输篇课后挑战:深入挖掘 NMEA 中的日期时间、海拔与速度等 GPS 扩展数据
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
本指南对应 IoT-For-Beginners 项目运输(Transport)篇第 1 课"位置跟踪(Location tracking)"的课后作业,主题是:在已经能解析经纬度的基础上,进一步挖掘 GPS 传感器 NMEA 数据流中除位置之外的扩展信息——UTC 日期时间、海拔高度、对地速度等,并让它们在你的 IoT 设备上真正可用(上报为遥测数据,或用来校准设备时钟)。读完本文后,你将掌握 NMEA 0183 句子的字段级解读方法、基于pynmea2(Python)与TinyGPSPlus(Arduino)两种解码路径的扩展数据提取写法,以及虚拟设备场景下用 nmeagen.org 生成测试数据的方法,并理解本作业的评分标准。
作业任务全景:从"定位"走向"全量 GPS 数据"
本课的常规练习只把 GPS 传感器输出中的经纬度提取出来打印。而课后作业 assignment.md 要求更进一步:
- 研究 NMEA 句子中除位置以外的其他数据,并把它们用到自己的 IoT 设备上;
- 例如:能否拿到当前日期和时间?如果用的是微控制器,能否像上一个项目中用NTP 信号校时那样,用 GPS 数据设置时钟?
- 能否拿到海拔高度(相对于海平面的高度)或当前速度?
- 如果使用虚拟 IoT 设备,可以通过发送用 nmeagen.org 之类工具生成的 NMEA 句子来获取部分这类数据。
作业的出发点是一个客观事实:GPS 传感器输出的是标准化的 NMEA 数据流,其中不止包含经纬度——时间戳、海拔、速度、卫星数量、定位质量等信息都以明文文本形式存在于同一串句子中。能否"看到"并"用起来",取决于你对 NMEA 句子结构(lesson README)和解码库 API 的掌握程度。
背景:NMEA 0183 句子中到底有哪些数据
GPS 传感器输出的原始数据遵循NMEA 0183标准(由美国国家海洋电子协会 National Marine Electronics Association 制定)。每一条数据是一段以$开头、以换行符结尾的文本"句子":$后跟 2 个字符表示信号来源(如GP表示美国 GPS 系统、GN表示兼容 GNSS 多星座),再跟 3 个字符表示句子类型,其余为逗号分隔的字段。
课程 README 中列出的常见句子类型如下:
| 类型 | 说明 |
|---|---|
| GGA | GPS 定位数据(GPS Fix Data),包含经纬度、海拔,以及参与计算定位的可见卫星数 |
| ZDA | 当前日期与时间,包括本地时区信息 |
| GSV | 可见卫星详情(GPS 传感器能够检测到信号的卫星) |
课程中特别指出:GPS 数据本身就带时间戳,所以 IoT 设备完全可以从 GPS 传感器获取时间,而不必依赖 NTP 服务器或板载实时时钟(RTC)。这正是作业中"用 GPS 校时"一说的依据来源。
逐字段拆解一条真实 GGA 句子
课程 README 给出了完整示例句:
$GNGGA,020604.001,4738.538654,N,12208.341758,W,1,3,,164.7,M,-17.1,M,,*67按 GGA 标准字段顺序解读:
| 字段 | 值 | 含义 |
|---|---|---|
| 句子头 | $GNGGA | GN 多星座来源 + GGA 类型 |
| 时间 | 020604.001 | UTC 时间 02:06:04.001 |
| 纬度 | 4738.538654,N | 47°38.538654′N,即十进制度 47.6423109 |
| 经度 | 12208.341758,W | 122°08.341758′W,即十进制度 -122.1390293 |
| 定位质量 | 1 | 1 = 已获得 GPS 定位(fix) |
| 卫星数 | 3 | 3 颗卫星参与定位计算 |
| 水平精度因子 | (空) | HDOP 未提供 |
| 海拔 | 164.7,M | 海拔 164.7 米(高于海平面) |
| 大地水准面高度 | -17.1,M | 大地水准面与椭球面的差值 -17.1 米 |
| 差分信息 | 两个空字段 | 距最近 DGPS 更新的时间与台站号 |
| 校验 | *67 | 异或校验和 |
可以看到,海拔(164.7 米)和时间(02:06:04.001 UTC)本来就在 GGA 句子里,不需要额外硬件,只需要在解码时把它们读出来。日期(年/月/日)通常由 ZDA 或 RMC 句子携带;对地速度通常由 RMC(以节 knot 为单位)或 VTG 句子携带。
三条数据来源路线
作业面向三类硬件平台,数据来源各不相同,但最终得到的都是同一种 NMEA 句子流:
物理传感器(Wio Terminal / Raspberry Pi):使用 Grove GPS Air530 传感器(UART 接口,可连接多个 GPS 星座以快速获得精确定位)。接线与读取方法分别见 wio-terminal-gps-sensor.md 与 pi-gps-sensor.md。传感器通过 UART 持续输出 NMEA 句子,波特率 9600,例如:
$GNGGA,020604.001,4738.538654,N,12208.341758,W,1,3,,164.7,M,-17.1,M,,*67 $GPGSA,A,1,,,,,,,,,,,,,,,*1E $GPGSV,1,1,00*79虚拟 IoT 设备(CounterFit):在 CounterFit 应用中创建UART GPS传感器(端口保持
/dev/ttyAMA0),随后可在Source中选择三种数据注入方式(详见 virtual-device-gps-sensor.md):Lat/Lon:手动指定经纬度与卫星数;NMEA:直接粘贴 NMEA 句子文本(建议勾选Repeat让数据每秒重复发送)——作业中提到的 nmeagen.org 就是用于在网页地图上绘制轨迹、自动生成整套 NMEA 句子的工具,这是虚拟设备路线获取日期、海拔、速度等扩展数据的最直接手段;- GPX 文件:上传包含多条轨迹点的 GPX 文件,传感器按 1 秒间隔依次返回每个位置。
数据流在 Python 端通过
counterfit-shims-serial包的串口模拟层读取(见 code-gps/virtual-device 下的完整代码)。与上一项目 NTP 校时对照:作业提示"像用 NTP 信号设置时钟那样用 GPS 数据校时"。在农场项目第 4 课 wio-terminal-connect-hub.md 及其 ntp.h 实现中,设备通过 NTP 服务器获取网络时间并校准本地时钟;而 GPS 路线的优势在于无需联网——只要天线能看到天空,NMEA 句子自带 UTC 时间戳,即可同样完成校时。这也解释了为什么 GGA 句子把 UTC 时间放在第一个字段。
实战一:Python 路线(Raspberry Pi / 虚拟设备)提取扩展数据
课程的解码任务使用pynmea2库解析串口行数据。仓库中的完整实现见 code-gps-decode/pi/gps-sensor/app.py 与 code-gps-decode/virtual-device/gps-sensor/app.py,核心逻辑如下:
import time import serial # Raspberry Pi 使用 pyserial;虚拟设备使用 counterfit_shims_serial import pynmea2 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': 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')安装依赖:pip3 install pynmea2(虚拟设备额外需pip install counterfit-shims-serial,参考 virtual-device-gps-sensor.md)。运行成功的输出形如:
47.6423109,-122.1390293 - from 3 satellites向作业目标扩展:读出时间、海拔与速度
pynmea2.parse会按句子类型把字段映射为对象属性,作业涉及的扩展数据正好落在这些属性上:
- GGA 句子:
msg.timestamp(UTC 时间)、msg.altitude(海拔,单位米)、msg.altitude_units、msg.num_sats(卫星数); - ZDA 句子:
msg.timestamp连同msg.day、msg.month、msg.year(完整日期),以及msg.local_zone_hours/msg.local_zone_minutes(本地时区); - RMC 句子:
msg.spd_over_grnd(对地速度,单位为节 knot),msg.true_course(真航向),msg.datestamp(日期)。
因此,在既有解码循环里只需按句子类型分别处理,即可把扩展数据一并产出,例如:
msg = pynmea2.parse(line) if msg.sentence_type == 'GGA': # 既有经纬度逻辑…… if msg.altitude is not None: print(f'altitude: {msg.altitude}{msg.altitude_units}') # 例如 164.7M print(f'UTC time: {msg.timestamp}') elif msg.sentence_type == 'ZDA': print(f'date: {msg.year}-{msg.month}-{msg.day}, zone: {msg.local_zone_hours}h') elif msg.sentence_type == 'RMC': speed_kmh = msg.spd_over_grnd * 1.852 # 节 → 公里/小时 print(f'speed: {speed_kmh:.1f} km/h, course: {msg.true_course}')说明:RMC 的速度字段以节为单位,
1 节 = 1.852 km/h;若设备需要网络无关的校时,把 GGA/ZDA 解析出的 UTC 时间换算为本地时间并写入时钟即可,原理与 ntp.h 中的 NTP 校时流程一致,只是时间来源从网络变成了 GPS 卫星。
实战二:Arduino 路线(Wio Terminal)提取扩展数据
Wio Terminal 的解码任务使用开源库TinyGPSPlus(在 platformio.ini 中以mikalhart/TinyGPSPlus @ 1.0.2声明依赖),完整代码见 code-gps-decode/wio-terminal/gps-sensor/src/main.cpp。其核心是逐字节喂给解码器:
#include <TinyGPS++.h> TinyGPSPlus gps; void printGPSData() { if (gps.encode(Serial3.read())) { if (gps.location.isValid()) { Serial.print(gps.location.lat(), 6); Serial.print(F(",")); Serial.print(gps.location.lng(), 6); Serial.print(" - from "); Serial.print(gps.satellites.value()); Serial.println(" satellites"); } } }TinyGPSPlus是一个持续累积状态的对象:每读入一个字符调用一次gps.encode(),当累积出有效句子时返回true。与pynmea2的"整句解析"不同,TinyGPSPlus 会把所有句子的数据汇总到gps对象上,因此扩展数据可直接通过成员访问:
gps.time.hour()/gps.time.minute()/gps.time.second()—— UTC 时间;gps.date.day()/gps.date.month()/gps.date.year()—— 日期;gps.altitude.meters()—— 海拔(米);gps.speed.kmph()(或knots()、mph())—— 对地速度;gps.satellites.value()—— 定位所用卫星数(代码中已用到)。
例如在printGPSData中追加:
if (gps.time.isValid() && gps.date.isValid()) { Serial.print("UTC: "); Serial.print(gps.date.year()); Serial.print("-"); Serial.print(gps.date.month()); Serial.print("-"); Serial.print(gps.date.day()); Serial.print(" "); Serial.print(gps.time.hour()); Serial.print(":"); Serial.print(gps.time.minute()); Serial.print(":"); Serial.println(gps.time.second()); } if (gps.altitude.isValid()) { Serial.print("altitude: "); Serial.print(gps.altitude.meters(), 1); Serial.println(" m"); } if (gps.speed.isValid()) { Serial.print("speed: "); Serial.print(gps.speed.kmph(), 1); Serial.println(" km/h"); }由于 UART 串口初始化、SERCOM3中断重定向等接线代码与课程既有工程一致(见 code-gps-decode/wio-terminal/gps-sensor/src/main.cpp),作业只需在上述解码函数内扩展输出即可,无需改动硬件接线。
用评分标准自检:你的作业处于哪个等级
作业的 Rubric(评分标准)只有一条维度,评价关键在"能否获取"与"能否使用"的区分:
| 标准 | 优秀 | 达标 | 待改进 |
|---|---|---|---|
| 获取更多 GPS 数据 | 能获取并实际使用更多 GPS 数据——作为遥测上报,或用来配置/校准 IoT 设备 | 能获取更多 GPS 数据,但无法把它们用起来 | 无法获取更多 GPS 数据 |
据此,评判作业完成度的关键不在"打印出了多少字段",而在数据是否真正进入了设备的业务逻辑。从本课代码结构看(single-board-computer-gps-decode.md),三种"使用"方向值得参考:
- 作为遥测数据上报:把 UTC 时间、海拔、速度与经纬度一起打包发送到 IoT Hub 等云端,扩展车队遥测维度;
- 用于设备校时:无网络环境下,用 GPS 时间戳替代 NTP/RTC 校准时钟(对照农场项目 ntp.h 的实现思路);
- 触发设备行为:例如以海拔或速度阈值判断运输途中状态(静止/行驶/爬坡),为后续课程的车队管理与地理围栏功能打基础。
小结
NMEA 0183 数据流是"位置跟踪"课程最富信息量的副产品:仅凭一条 GGA 句子就能同时获得 UTC 时间、经纬度、卫星数和海拔,配合 ZDA/RMC 句子还能补齐完整日期与对地速度。本作业的实质是从"能用经纬度"升级到"能用整条 NMEA 数据流":Python 端利用pynmea2按句子类型取字段,Arduino 端利用TinyGPSPlus的状态累积对象直接访问时间、日期、海拔与速度成员;虚拟设备场景则可用 nmeagen.org 生成包含全部扩展字段的 NMEA 句子注入 CounterFit 验证。想继续深挖,可回读本课主文档 README.md 中关于 GGA/ZDA/GSV 三类句子的定义,以及解码部分的 single-board-computer-gps-decode.md 与 wio-terminal-gps-decode.md。
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考