从GPS到北斗:手把手教你用Python解析NMEA-0183数据(附完整代码)
从GPS到北斗:手把手教你用Python解析NMEA-0183数据(附完整代码)
当你第一次拿到一个GNSS模块,通过串口接收到类似$GNGGA,082923.00,3901.106815,N,11712.322006,E,1,12,1.0,60.6,M,-4.0,M,,*5A这样的文本时,是否感到既兴奋又困惑?这些看似杂乱的数据串,实际上包含着经纬度、海拔高度、卫星数量等关键定位信息。本文将带你从零开始,用Python构建一个专业的NMEA-0183协议解析器。
1. GNSS与NMEA-0183基础认知
全球导航卫星系统(GNSS)是一个统称,涵盖了美国的GPS、俄罗斯的GLONASS、欧洲的Galileo以及中国的北斗系统。这些系统通过卫星发射信号,地面接收器通过计算信号传播时间来确定位置。
NMEA-0183是美国国家海洋电子协会制定的标准协议,已成为GNSS设备的通用数据格式。它采用ASCII文本格式,每条语句以$开头,以*和校验和结束。常见语句类型包括:
- GGA:时间、位置、定位质量数据
- RMC:推荐最小定位信息(包含速度)
- GSV:可见卫星信息
- GSA:当前卫星状态
# 典型NMEA语句示例 nmea_samples = [ "$GNGGA,082923.00,3901.106815,N,11712.322006,E,1,12,1.0,60.6,M,-4.0,M,,*5A", "$GNRMC,082923.00,A,3901.106815,N,11712.322006,E,0.0,,231121,5.8,W,A,V*61" ]2. 搭建Python解析框架
我们需要构建一个能够处理多种NMEA语句的解析系统。首先创建基础解析类:
import re from typing import Dict, List, Optional class NMEAParser: def __init__(self): self.sentence_handlers = { "GGA": self._parse_gga, "RMC": self._parse_rmc, "GSV": self._parse_gsv, "GSA": self._parse_gsa } self._validate_checksum = True def parse(self, sentence: str) -> Optional[Dict]: if not sentence.startswith('$'): return None # 校验和验证 if self._validate_checksum and not self._verify_checksum(sentence): raise ValueError("Invalid checksum") # 提取语句类型 talker, sentence_type = sentence[1:3], sentence[3:6].lstrip(',') if sentence_type not in self.sentence_handlers: return None # 分割数据字段 fields = sentence.split(',') return self.sentence_handlers[sentence_type](talker, fields) def _verify_checksum(self, sentence: str) -> bool: """验证NMEA校验和""" try: check_start = sentence.index('*') + 1 checksum = int(sentence[check_start:check_start+2], 16) calculated = 0 for char in sentence[1:check_start-1]: calculated ^= ord(char) return calculated == checksum except ValueError: return False3. 核心语句解析实现
3.1 GGA语句解析
GGA(Global Positioning System Fix Data)提供最基础的定位信息:
def _parse_gga(self, talker: str, fields: List[str]) -> Dict: """解析GGA语句示例: $GNGGA,082923.00,3901.106815,N,11712.322006,E,1,12,1.0,60.6,M,-4.0,M,,*5A """ if len(fields) < 14: raise ValueError("Invalid GGA sentence") # 度分格式转换为十进制 def dm_to_decimal(dm: str, hemisphere: str) -> float: if not dm or not hemisphere: return None deg = float(dm[:2]) if len(dm) > 4 else float(dm[:3]) minutes = float(dm[2:]) if len(dm) > 4 else float(dm[3:]) decimal = deg + minutes / 60 return -decimal if hemisphere in ['S', 'W'] else decimal return { "type": "GGA", "talker": talker, "timestamp": fields[1], "latitude": dm_to_decimal(fields[2], fields[3]), "longitude": dm_to_decimal(fields[4], fields[5]), "quality": { 0: "invalid", 1: "GPS", 2: "DGPS", 3: "PPS", 4: "RTK", 5: "Float RTK" }.get(int(fields[6]), "unknown"), "satellites": int(fields[7]), "hdop": float(fields[8]) if fields[8] else None, "altitude": float(fields[9]) if fields[9] else None, "geoid_separation": float(fields[11]) if fields[11] else None }3.2 RMC语句解析
RMC(Recommended Minimum Specific GNSS Data)包含移动物体的关键信息:
def _parse_rmc(self, talker: str, fields: List[str]) -> Dict: """解析RMC语句示例: $GNRMC,082923.00,A,3901.106815,N,11712.322006,E,0.0,,231121,5.8,W,A,V*61 """ if len(fields) < 12: raise ValueError("Invalid RMC sentence") return { "type": "RMC", "talker": talker, "timestamp": fields[1], "status": "active" if fields[2] == 'A' else "void", "latitude": self._dm_to_decimal(fields[3], fields[4]), "longitude": self._dm_to_decimal(fields[5], fields[6]), "speed_knots": float(fields[7]) if fields[7] else None, "true_course": float(fields[8]) if fields[8] else None, "date": f"20{fields[9][4:6]}-{fields[9][2:4]}-{fields[9][0:2]}", "magnetic_variation": float(fields[10]) if fields[10] else None, "mode": { 'A': "autonomous", 'D': "differential", 'E': "estimated", 'N': "invalid" }.get(fields[11][0] if fields[11] else 'N', "invalid") }4. 多系统支持与数据融合
现代GNSS模块往往支持多系统联合定位,我们需要识别不同系统的卫星:
def _parse_gsv(self, talker: str, fields: List[str]) -> Dict: """解析GSV语句示例: $GPGSV,3,1,09,16,26,218,19,29,29,071,38,31,66,027,33,32,40,140,24,1*63 """ system_map = { 'GP': 'GPS', 'GL': 'GLONASS', 'GA': 'Galileo', 'GB': 'Beidou', 'GN': 'Multi-GNSS' } if len(fields) < 4: raise ValueError("Invalid GSV sentence") satellites = [] for i in range(4, len(fields)-3, 4): if not fields[i]: break satellites.append({ "prn": int(fields[i]), "elevation": int(fields[i+1]), "azimuth": int(fields[i+2]), "snr": int(fields[i+3]) if fields[i+3] else None }) return { "type": "GSV", "system": system_map.get(talker, talker), "total_messages": int(fields[1]), "message_number": int(fields[2]), "satellites_in_view": int(fields[3]), "satellites": satellites }5. 实战:构建完整数据处理流程
现在我们将所有组件整合成一个完整的处理系统:
import serial from queue import Queue from threading import Thread import json class GNSSReceiver: def __init__(self, port: str, baudrate: int = 9600): self.serial = serial.Serial(port, baudrate, timeout=1) self.parser = NMEAParser() self.data_queue = Queue() self.running = False def start(self): self.running = True self.thread = Thread(target=self._read_serial) self.thread.start() def stop(self): self.running = False self.thread.join() self.serial.close() def _read_serial(self): buffer = "" while self.running: data = self.serial.readline().decode('ascii', errors='ignore').strip() if not data: continue # 处理多语句情况 for line in data.split('\n'): line = line.strip() if line.startswith('$'): try: result = self.parser.parse(line) if result: self.data_queue.put(result) except ValueError as e: print(f"Parse error: {e}") def get_data(self, timeout: float = 1.0) -> Dict: try: return self.data_queue.get(timeout=timeout) except: return None # 使用示例 if __name__ == "__main__": receiver = GNSSReceiver('/dev/ttyUSB0') # 修改为你的实际端口 receiver.start() try: while True: data = receiver.get_data() if data: print(json.dumps(data, indent=2)) except KeyboardInterrupt: receiver.stop()6. 高级功能扩展
6.1 数据校验与纠错
在实际应用中,我们需要增强系统的鲁棒性:
def enhanced_parse(self, sentence: str) -> Optional[Dict]: # 预处理:移除无关字符 sentence = re.sub(r'[^\x20-\x7E]', '', sentence).strip() # 验证基本结构 if not re.match(r'^\$[A-Z]{2}[A-Z]{3},.*\*[0-9A-F]{2}$', sentence): return None # 使用基础解析 result = self.parse(sentence) # 后处理验证 if result and result.get('type') == 'GGA': if not (0 <= result.get('satellites', -1) <= 32): return None return result6.2 实时位置可视化
结合Matplotlib实现简单的位置轨迹绘制:
import matplotlib.pyplot as plt from collections import deque class PositionTracker: def __init__(self, max_points=100): self.fig, self.ax = plt.subplots() self.lat_points = deque(maxlen=max_points) self.lon_points = deque(maxlen=max_points) self.line, = self.ax.plot([], [], 'b-') def update(self, lat, lon): if lat is not None and lon is not None: self.lat_points.append(lat) self.lon_points.append(lon) self.line.set_data(self.lon_points, self.lat_points) self.ax.relim() self.ax.autoscale_view() plt.pause(0.01)7. 性能优化技巧
处理高频NMEA数据时,这些优化策略很关键:
- 批量处理:累积多条语句后统一处理
- 选择性解析:只处理需要的语句类型
- 缓存机制:避免重复解析相同字段
- 多线程处理:I/O与计算分离
from functools import lru_cache class OptimizedParser(NMEAParser): @lru_cache(maxsize=128) def _dm_to_decimal(self, dm: str, hemisphere: str) -> float: """带缓存的度分转换""" if not dm or not hemisphere: return None deg = float(dm[:2]) if len(dm) > 4 else float(dm[:3]) minutes = float(dm[2:]) if len(dm) > 4 else float(dm[3:]) decimal = deg + minutes / 60 return -decimal if hemisphere in ['S', 'W'] else decimal8. 实际应用案例
8.1 无人机飞行轨迹记录
class DroneTracker: def __init__(self): self.parser = NMEAParser() self.position_history = [] self.current_status = {} def process_data(self, sentence): result = self.parser.parse(sentence) if not result: return if result['type'] == 'RMC': self.current_status.update({ 'timestamp': result['timestamp'], 'latitude': result['latitude'], 'longitude': result['longitude'], 'speed': result['speed_knots'] * 1.852 if result['speed_knots'] else None # 节转km/h }) self.position_history.append(( result['latitude'], result['longitude'] )) elif result['type'] == 'GGA': self.current_status.update({ 'altitude': result['altitude'], 'satellites': result['satellites'], 'hdop': result['hdop'] })8.2 车载导航数据融合
class VehicleNavigationSystem: def __init__(self): self.parser = NMEAParser() self.current_position = None self.satellite_info = {} def update(self, sentence): data = self.parser.parse(sentence) if data['type'] == 'GGA': self.current_position = { 'lat': data['latitude'], 'lon': data['longitude'], 'alt': data['altitude'], 'time': data['timestamp'] } elif data['type'] == 'GSV': system = data['system'] if system not in self.satellite_info: self.satellite_info[system] = [] self.satellite_info[system].extend(data['satellites']) def get_signal_quality(self): total_snr = sum(sat['snr'] for sats in self.satellite_info.values() for sat in sats if sat['snr']) count = sum(1 for sats in self.satellite_info.values() for sat in sats if sat['snr']) return total_snr / count if count else 09. 异常处理与调试
健壮的解析器需要完善的错误处理机制:
class RobustParser(NMEAParser): def parse(self, sentence: str) -> Optional[Dict]: try: # 基础验证 if not sentence or len(sentence) < 6: return None # 校验和检查 if self._validate_checksum and not self._verify_checksum(sentence): raise ValueError("Checksum mismatch") # 提取语句类型 parts = sentence[1:].split(',', 1) if not parts or len(parts[0]) < 2: return None talker = parts[0][:2] sentence_type = parts[0][2:5] if len(parts[0]) > 2 else None # 调用具体解析器 if sentence_type in self.sentence_handlers: fields = sentence.split(',') return self.sentence_handlers[sentence_type](talker, fields) except (ValueError, IndexError, AttributeError) as e: print(f"Error parsing '{sentence}': {str(e)}") return None10. 完整代码整合
以下是整合后的完整解析器代码,包含所有核心功能:
""" Complete NMEA-0183 Parser for GNSS Data Supports GPS, GLONASS, Galileo and BeiDou systems """ import re from typing import Dict, List, Optional, Deque from collections import defaultdict, deque from dataclasses import dataclass import logging # 配置日志 logging.basicConfig(level=logging.INFO) logger = logging.getLogger("NMEAParser") @dataclass class Position: latitude: float longitude: float altitude: Optional[float] = None timestamp: Optional[str] = None class NMEAParser: def __init__(self, validate_checksum: bool = True): self.validate_checksum = validate_checksum self.sentence_handlers = { "GGA": self._parse_gga, "RMC": self._parse_rmc, "GSV": self._parse_gsv, "GSA": self._parse_gsa, "VTG": self._parse_vtg } self.system_map = { 'GP': 'GPS', 'GL': 'GLONASS', 'GA': 'Galileo', 'GB': 'BeiDou', 'GN': 'Multi-GNSS', 'BD': 'BeiDou', 'GQ': 'QZSS' } def parse(self, sentence: str) -> Optional[Dict]: """主解析方法""" try: sentence = sentence.strip() if not self._validate(sentence): return None talker, sentence_type = self._get_sentence_type(sentence) if not sentence_type: return None fields = sentence.split(',') return self.sentence_handlers[sentence_type](talker, fields) except Exception as e: logger.error(f"Error parsing '{sentence}': {str(e)}") return None def _validate(self, sentence: str) -> bool: """验证NMEA语句基本有效性""" if not sentence.startswith('$'): return False if '*' not in sentence: return False if self.validate_checksum and not self._verify_checksum(sentence): logger.warning(f"Checksum failed for: {sentence}") return False return True def _verify_checksum(self, sentence: str) -> bool: """验证校验和""" try: check_start = sentence.index('*') + 1 checksum = int(sentence[check_start:check_start+2], 16) calculated = 0 for char in sentence[1:check_start-1]: calculated ^= ord(char) return calculated == checksum except ValueError: return False def _get_sentence_type(self, sentence: str) -> tuple: """提取语句类型""" parts = sentence[1:].split(',', 1) if not parts or len(parts[0]) < 2: return None, None return parts[0][:2], parts[0][2:] def _dm_to_decimal(self, dm: str, hemisphere: str) -> Optional[float]: """度分格式转十进制""" if not dm or not hemisphere: return None try: point_pos = dm.index('.') degrees = float(dm[:point_pos-2]) minutes = float(dm[point_pos-2:]) decimal = degrees + minutes / 60 return -decimal if hemisphere in ['S', 'W'] else decimal except ValueError: return None def _parse_gga(self, talker: str, fields: List[str]) -> Dict: """解析GGA语句""" if len(fields) < 14: raise ValueError("Invalid GGA format") return { "type": "GGA", "system": self.system_map.get(talker, talker), "timestamp": fields[1], "latitude": self._dm_to_decimal(fields[2], fields[3]), "longitude": self._dm_to_decimal(fields[4], fields[5]), "quality": { 0: "invalid", 1: "GPS", 2: "DGPS", 3: "PPS", 4: "RTK", 5: "Float RTK", 6: "estimated", 7: "manual", 8: "simulation" }.get(int(fields[6]), "unknown"), "satellites": int(fields[7]) if fields[7] else 0, "hdop": float(fields[8]) if fields[8] else None, "altitude": float(fields[9]) if fields[9] else None, "geoid_separation": float(fields[11]) if fields[11] else None, "age": float(fields[13]) if len(fields) > 13 and fields[13] else None } # 其他解析方法保持不变... def batch_parse(self, sentences: List[str]) -> List[Dict]: """批量解析多条语句""" return [res for res in (self.parse(s) for s in sentences) if res] class GNSSProcessor: """高级GNSS数据处理类""" def __init__(self): self.parser = NMEAParser() self.position_history: Deque[Position] = deque(maxlen=1000) self.current_status: Dict = {} self.satellites: Dict[str, List] = defaultdict(list) def update(self, sentence: str): """更新当前状态""" data = self.parser.parse(sentence) if not data: return # 更新位置信息 if data['type'] in ('GGA', 'RMC'): pos = Position( latitude=data.get('latitude'), longitude=data.get('longitude'), altitude=data.get('altitude'), timestamp=data.get('timestamp') ) if pos.latitude and pos.longitude: self.position_history.append(pos) # 更新卫星信息 if data['type'] == 'GSV': system = data.get('system', 'unknown') self.satellites[system] = data.get('satellites', []) def get_current_position(self) -> Optional[Position]: """获取最新有效位置""" for pos in reversed(self.position_history): if pos.latitude and pos.longitude: return pos return None11. 测试与验证
为确保解析器准确性,我们需要构建测试用例:
import unittest class TestNMEAParser(unittest.TestCase): def setUp(self): self.parser = NMEAParser() def test_gga_parsing(self): sample = "$GNGGA,082923.00,3901.106815,N,11712.322006,E,1,12,1.0,60.6,M,-4.0,M,,*5A" result = self.parser.parse(sample) self.assertAlmostEqual(result['latitude'], 39.018447, places=6) self.assertAlmostEqual(result['longitude'], 117.205367, places=6) self.assertEqual(result['altitude'], 60.6) def test_rmc_parsing(self): sample = "$GNRMC,082923.00,A,3901.106815,N,11712.322006,E,0.0,,231121,5.8,W,A,V*61" result = self.parser.parse(sample) self.assertEqual(result['date'], "2021-11-23") self.assertAlmostEqual(result['speed_knots'], 0.0) def test_invalid_checksum(self): sample = "$GNGGA,082923.00,3901.106815,N,11712.322006,E,1,12,1.0,60.6,M,-4.0,M,,*00" result = self.parser.parse(sample) self.assertIsNone(result) if __name__ == "__main__": unittest.main()12. 性能基准测试
使用timeit模块评估解析性能:
import timeit def benchmark(): parser = NMEAParser() samples = [ "$GNGGA,082923.00,3901.106815,N,11712.322006,E,1,12,1.0,60.6,M,-4.0,M,,*5A", "$GNRMC,082923.00,A,3901.106815,N,11712.322006,E,0.0,,231121,5.8,W,A,V*61", "$GPGSV,3,1,09,16,26,218,19,29,29,071,38,31,66,027,33,32,40,140,24,1*63" ] def test(): for sample in samples: parser.parse(sample) time = timeit.timeit(test, number=10000) print(f"Parsed {len(samples)*10000} sentences in {time:.2f}s") print(f"{len(samples)*10000/time:.0f} sentences/second") benchmark()13. 实际硬件连接示例
连接U-blox NEO-8M模块的完整示例:
import serial from serial.tools import list_ports def find_gnss_port(): """自动识别GNSS设备端口""" for port in list_ports.comports(): if 'u-blox' in port.description or 'USB Serial' in port.description: return port.device return None def main(): port = find_gnss_port() or '/dev/ttyACM0' # 默认端口 processor = GNSSProcessor() try: with serial.Serial(port, baudrate=9600, timeout=1) as ser: print(f"Connected to {port}, waiting for data...") while True: line = ser.readline().decode('ascii', errors='ignore').strip() if line: processor.update(line) pos = processor.get_current_position() if pos: print(f"Position: {pos.latitude:.6f}, {pos.longitude:.6f}") except KeyboardInterrupt: print("\nExiting...") except Exception as e: print(f"Error: {str(e)}") if __name__ == "__main__": main()14. 数据持久化方案
将解析后的数据存储到SQLite数据库:
import sqlite3 from contextlib import contextmanager from datetime import datetime @contextmanager def db_connection(db_path='gnss_data.db'): conn = sqlite3.connect(db_path) try: yield conn finally: conn.close() class GNSSDataLogger: def __init__(self): self._init_db() def _init_db(self): with db_connection() as conn: conn.execute(""" CREATE TABLE IF NOT EXISTS positions ( id INTEGER PRIMARY KEY, timestamp TEXT NOT NULL, latitude REAL NOT NULL, longitude REAL NOT NULL, altitude REAL, satellites INTEGER, hdop REAL, source TEXT ) """) def log_position(self, data: Dict): if not data or 'latitude' not in data or 'longitude' not in data: return with db_connection() as conn: conn.execute(""" INSERT INTO positions ( timestamp, latitude, longitude, altitude, satellites, hdop, source ) VALUES (?, ?, ?, ?, ?, ?, ?) """, ( data.get('timestamp') or datetime.utcnow().isoformat(), data['latitude'], data['longitude'], data.get('altitude'), data.get('satellites'), data.get('hdop'), data.get('system', 'unknown') ))15. 跨平台兼容性处理
确保代码在Windows/Linux/macOS上都能运行:
import platform import serial def get_serial_port(port=None): """获取可用的串口设备""" system = platform.system() if port: return port # 常见GNSS模块的默认端口 defaults = { 'Linux': '/dev/ttyACM0', 'Darwin': '/dev/cu.usbmodem14101', 'Windows': 'COM3' } # 尝试自动检测 try: ports = list(serial.tools.list_ports.comports()) if ports: return ports[0].device except: pass return defaults.get(system, None) def configure_serial(port): """配置串口参数""" return serial.Serial( port=port, baudrate=9600, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=1, xonxoff=False, rtscts=False, dsrdtr=False )16. 与Web服务集成
将GNSS数据发布到Web服务:
import requests from threading import Timer class GNSSWebPublisher: def __init__(self, api_url, interval=10): self.api_url = api_url self.interval = interval self.buffer = [] self.timer = None def add_data(self, data): """添加数据到缓冲区""" if data and 'latitude' in data and 'longitude' in data: self.buffer.append(data) def start(self): """启动定时发布""" self._publish() def _publish(self): """发布数据到Web服务""" if self.buffer: try: response = requests.post( self.api_url, json={"positions": self.buffer}, timeout=5 ) if response.status_code == 200: self.buffer = [] except Exception as e: logger.error(f"Publish failed: {str(e)}") self.timer = Timer(self.interval, self._publish) self.timer.start() def stop(self): """停止发布""" if self.timer: self.timer.cancel()17. 安全注意事项
处理GNSS数据时的安全最佳实践:
- 数据验证:始终验证输入数据的完整性和有效性
- 校验和检查:不要禁用校验和验证
- 缓冲区管理:防止缓冲区溢出攻击
- 敏感信息:位置数据可能敏感,注意隐私保护
class SecureGNSSParser(NMEAParser): def __init__(self): super().__init__() self.max_sentence_length = 82 # NMEA标准最大长度 def parse(self, sentence: str) -> Optional[Dict]: # 长度检查 if len(sentence) > self.max_sentence_length: logger.warning("Sentence too long, possible attack") return None # 字符集检查 if not all(32 <= ord(c) <= 126 for c in sentence): logger.warning("Invalid characters in sentence") return None return super().parse(sentence)18. 未来扩展方向
- RTCM协议支持:增加差分GPS支持
- 惯导融合:结合IMU数据进行航位推算
- 机器学习:异常检测和信号质量评估
- Web界面:实时位置可视化
class AdvancedGNSSProcessor(GNSSProcessor): def __init__(self): super().__init__() self.velocity_history = deque(maxlen=10) def estimate_position(self, delta_time: float) -> Optional[Position]: """基于速度和航向估算当前位置""" if len(self.velocity_history) < 2: return None # 简单线性估算 avg_speed = sum(v['speed'] for v in self.velocity_history) / len(self.velocity_history) avg_course = sum(v['course'] for v in self.velocity_history) / len(self.velocity_history) last_pos = self.get_current_position() if not last_pos: return None # 简化的位置推算(不考虑地球曲率) distance = avg_speed * delta_time / 3600 # 转为小时 rad = math.radians(avg_course) delta_lat = distance * math.cos(rad) delta_lon = distance * math.sin(rad) return Position( latitude=last_pos.latitude + delta_lat, longitude=last_pos.longitude + delta_lon, altitude=last_pos.altitude )19. 常见问题解决
问题1:接收到的数据不完整或乱码
解决方案:
def read_robust(ser: serial.Serial, max_attempts=3) -> Optional[str]: """更健壮的串口读取方法""" attempt = 0 while attempt < max_attempts: try: data = ser.readline().decode('ascii').strip() if data.startswith('$') and '*' in data: return data except UnicodeDecodeError: ser.reset_input_buffer() attempt += 1 return None问题2:位置数据跳动严重
解决方案:
def smooth_position(position_history: Deque[Position], window_size=5) -> Position: """滑动平均平滑位置数据""" if len(position_history) < window_size: return position_history[-1] if position_history else None recent = list(position_history)[-window_size:] avg_lat = sum(p.latitude for p in recent) / window_size avg_lon = sum(p.longitude for p in recent) / window_size avg_alt = sum(p.altitude for p in recent) / window_size if all(p.altitude for p in recent) else None return Position( latitude=avg_lat, longitude=avg_lon, altitude=avg_alt, timestamp=recent[-1].timestamp )20. 资源与进阶学习
官方文档:
- NMEA 0183标准协议
- U-blox协议规范
开源项目:
- gpsd :专业的GPS服务守护进程
- pySerial :Python串口通信库
硬件推荐:
- U-blox NEO-8M:性价比高的多模GNSS模块
- Quectel L86:支持北斗的低功耗模块
- SwiftNav Piksi Multi:高精度RTK接收器
# 示例:检测支持的GNSS系统 def detect_gnss_systems(parser: NMEAParser, samples: List[str]) -> set: systems = set() for sample in samples: data = parser.parse(sample) if data and