news 2026/10/12 2:41:17

GPS定位地图C#实现:串口读取、NMEA解析与坐标转换全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GPS定位地图C#实现:串口读取、NMEA解析与坐标转换全解析

简介:面向C#开发者和GIS入门者的GPS定位地图Demo源码包,演示如何基于.NET框架读取GPS/NMEA数据,并结合Bing Maps地图控件实现城市定位、缩放和平移等常见功能。rar压缩包共61个文件,约235KB,包括29个C#源码文件、6个资源文件及配套resx界面定义,另有可执行exe、动态库dll和项目配置文件,构成可直接打开运行的完整Visual Studio解决方案。源码中划分了数据获取、设备状态、坐标转换与界面窗体等模块,覆盖GPS事件监听、经纬度转换、地图视图操作和城市名地理编码等关键环节,适合学习串口或GPS数据解析、WinForms地图集成及异步更新界面的实现思路。该资源已有727人学习下载,体积小、结构清晰,便于对照源码快速理解定位与地图联动的基本原理。

1. GPS定位地图C#实现:这套源码到底该包含哪几块

标题里的"GPS定位—地图C#实现源码",说穿了就是一套桌面定位工具的核心代码:从串口GPS模块收数据,解析出经纬度,再把动态位置画到地图上。做车载定位、外勤巡检、设备追踪这类WinForms工具的人,最容易卡住的不是串口读取,而是坐标一上地图就漂出去几百米。判断一套现成源码值不值得用,我只看四块齐不齐:串口缓冲读取、NMEA语句解析、WGS-84与GCJ-02坐标转换、地图标记与轨迹绘制。缺了任何一块,都只能算一个会打印坐标的假定位程序。下面按这个顺序,把每块的实现、参数和踩过的坑讲透,你照着写也能跑通一套能用的定位地图程序。

2. 串口读取GPS数据:NMEA协议解析与串口防丢帧

2.1 串口参数与接线:9600波特率只解决一半问题

市面上的串口GPS模块,默认输出基本都是9600、8、N、1,也就是波特率9600、8位数据位、无校验、1位停止位。模块上电后每秒输出一条NMEA语句流,包含GPGGA、GPRMC、GPGSV这几类语句。接线时除了VCC和GND,重点是模块的TX接电脑端的RX,交叉接,直连最常见的现象就是读出来全是乱码或干脆没数据。

C#侧初始化串口的代码不难,难点在参数与线程:

using System.IO.Ports; // GPS定位Demo:串口初始化 SerialPort gpsPort = new SerialPort { PortName = "COM3", // 以设备管理器里枚举到的实际串口为准 BaudRate = 9600, // 出厂默认波特率,部分模块支持115200,需同步改 Parity = Parity.None, DataBits = 8, StopBits = StopBits.One, ReadTimeout = 1000, // 读超时,防止ReadLine阻塞调用线程 WriteTimeout = 1000 }; gpsPort.DataReceived += OnGpsDataReceived; // 数据到达事件 gpsPort.Open();

逻辑说明:SerialPort的DataReceived事件在后台线程触发,不是UI线程,事件里不能直接碰TextBox、Label这类控件,否则会频繁抛跨线程异常。我一般只在这里做数据积累和解析,结果通过BeginInvoke抛回UI线程更新,第4章会展开。

参数说明:如果用的是.NET Core或.NET 5+,System.IO.Ports需要单独引包,.NET Framework自带。ReadTimeout和WriteTimeout建议都设上,防止某些模块上电瞬间握手异常导致ReadLine永久卡住。PortName别写死,程序启动时枚举SerialPort.GetPortNames()让用户选,否则换台电脑就翻车。

2.2 解析GPRMC和GPGGA:度分格式必须转成十进制度

NMEA语句里最容易忽略的坑是经纬度格式。GPRMC和GPGGA里的坐标是度分格式,比如4807.038代表48度07.038分,不是48.07038度。直接当小数用,点位会偏出去大约70公里,这个错我在模拟项目X早期版本里犯过,属于典型的坐标格式陷阱。

先记住两条常用语句的字段位置:

语句定位状态纬度纬向经度经向
GPRMC字段2(A有效/V无效)字段3字段4字段5字段6
GPGGA字段6(0无效/1定位)字段2字段3字段4字段5
// GPS定位Demo:单条NMEA语句解析 public static GpsFix ParseNmeaLine(string line) { if (!line.StartsWith("$GPRMC") && !line.StartsWith("$GPGGA")) return null; string[] f = line.Split(','); bool isRmc = line.StartsWith("$GPRMC"); // GPRMC字段2是状态位,GPGGA字段6是定位质量 string status = isRmc ? f[2] : f[6]; bool valid = isRmc ? status == "A" : status == "1"; if (!valid) return null; // V或0表示当前没有有效定位 int latIdx = isRmc ? 3 : 2; // 纬度值所在字段索引 int lonIdx = isRmc ? 5 : 4; // 经度值所在字段索引 if (f[latIdx].Length == 0 || f[lonIdx].Length == 0) return null; double lat = NmeaToDecimal(f[latIdx], f[latIdx + 1]); // 后一位是N/S double lon = NmeaToDecimal(f[lonIdx], f[lonIdx + 1]); // 后一位是E/W return new GpsFix { Latitude = lat, Longitude = lon }; } // 度分格式转十进制度:4807.038 => 48 + 7.038 / 60 private static double NmeaToDecimal(string value, string hemi) { double raw = double.Parse(value, System.Globalization.CultureInfo.InvariantCulture); int degrees = (int)(raw / 100); double minutes = raw - degrees * 100; double result = degrees + minutes / 60.0; return (hemi == "S" || hemi == "W") ? -result : result; }

逻辑说明:代码把两种语句统一处理,按语句类型取不同字段索引,定位状态无效直接返回null,不进入后续流程。NmeaToDecimal的关键是先把度分离出来,再算分钟的小数部分。

参数说明:double.Parse必须带InvariantCulture,否则在中文区域设置下小数点可能被解析成千分位,坐标会放大一千倍。南纬、西经要取负值,这个细节漏了会让轨迹画到镜面对称的位置。GpsFix建议放Latitude、Longitude两个double,加一个Timestamp备用。解析返回null要接受,冷启动阶段、隧道里、模块天线朝下时状态位就是无效,这不是程序bug,过滤掉就好。

2.3 DataReceived丢帧:用字符串缓冲拼接完整NMEA语句

串口DataReceived有个经典问题:一次事件到达的字节不一定是完整一行,可能半句也可能好几句。如果直接ReadLine硬读,偶尔会拿到半截语句导致解析错乱,这就是大家常说的串口玄学之一。我用的方案是拿StringBuilder做累积缓冲,每次只处理到最后一个换行符之前的内容,剩下的留在缓冲区里等下一次事件拼上。

private readonly StringBuilder nmeaBuffer = new StringBuilder(); private void OnGpsDataReceived(object sender, SerialDataReceivedEventArgs e) { string chunk = gpsPort.ReadExisting(); // 把当前缓冲区全部读出 nmeaBuffer.Append(chunk); string snapshot = nmeaBuffer.ToString(); int lastNl = snapshot.LastIndexOf('\n'); if (lastNl < 0) return; // 还没完整行,等下次事件 string completePart = snapshot.Substring(0, lastNl); nmeaBuffer.Remove(0, lastNl + 1); // 清掉已处理部分 string[] lines = completePart.Split('\n'); foreach (string rawLine in lines) { string line = rawLine.Trim(); if (line.StartsWith("$GPRMC") || line.StartsWith("$GPGGA")) { GpsFix fix = ParseNmeaLine(line); if (fix != null) BeginInvoke(new Action(() => UpdateMap(fix.Latitude, fix.Longitude))); } } }

逻辑说明:核心思路是"只消费完整行,半行留在缓冲区"。LastIndexOf找的是换行符而不是\r,因为部分模块输出\r\n,有些输出裸\n,统一按\n切分再Trim掉\r最省事。

参数说明:匹配语句时只认$GPRMC和$GPGGA两条,理由是这两条都携带经纬度,而$GPGSV只有卫星信息、$GPVTG只有速度航向。两句话同时解析并更新地图,会出现每秒两个点的抖动,所以实际项目里我建议二选一,优先GPRMC,因为同一行里还带速度、航向、日期,后面做日志回放时数据更全。BeginInvoke只针对有效定位调用,无效帧直接忽略,地图上的点不会因为一秒里的无效帧被反复横跳。

3. 坐标系转换:WGS-84转GCJ-02的C#实现与验证

3.1 为什么地图上会偏几十米:WGS-84与GCJ-02的偏移

GPS模块输出的坐标是WGS-84,而国内图商的网页端和客户端地图,用的坐标系是GCJ-02,也就是业内常说的加密偏移坐标。两者之间不是简单的加减常数,而是一套非线性偏移。直接拿没转换的WGS-84坐标画到GCJ-02地图上,点位通常偏出几十到几百米,运气差会落在路对面、河中央,轨迹画出来像醉汉走路。这是定位地图源码里最关键的认知门槛,很多初看到源码的人以为是自己GPS模块坏了,其实模块没毛病,就是坐标没换。

老手拿到一套源码,第一步就是找坐标系转换函数:搜代码里有没有wgs84ToGcj02、outOfChina这类函数名,或者有没有调用图商提供的坐标转换服务。两个都没有,那这套源码画出来的图基本不能直接用。还有一种情况是源码里坐标转换的调用位置不对,转了但没用在显示路径上,效果等同于没转。

3.2 Wgs84ToGcj02函数:直接可用的C#实现与常量说明

常见做法是在本地实现这套偏移算法,省得每次转换都调网络接口、还要处理限流。网上广泛流传的偏移算法是同一套公开公式,C#版本我在某定位项目里整理过,三个关键常量A(参考椭球长半轴)、Ee(偏心率平方)和outOfChina边界判断是固定的,照抄即可:

// GPS定位Demo:WGS-84 转 GCJ-02(国内图商坐标) private const double Pi = Math.PI; private const double A = 6378245.0; // 参考椭球长半轴,米 private const double Ee = 0.00669342162296594323; // 偏心率平方 // 粗略判断是否在国内范围,国外坐标不做偏移直接返回 private static bool OutOfChina(double lat, double lon) { return lon < 72.004 || lon > 137.8347 || lat < 0.8293 || lat > 55.8271; } private static double TransformLat(double x, double y) { double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.Sqrt(Math.Abs(x)); ret += (20.0 * Math.Sin(6.0 * x * Pi) + 20.0 * Math.Sin(2.0 * x * Pi)) * 2.0 / 3.0; ret += (20.0 * Math.Sin(y * Pi) + 40.0 * Math.Sin(y / 3.0 * Pi)) * 2.0 / 3.0; ret += (160.0 * Math.Sin(y / 12.0 * Pi) + 320.0 * Math.Sin(y * Pi / 30.0)) * 2.0 / 3.0; return ret; } private static double TransformLon(double x, double y) { double ret = 300.0 + x + 2.0 * y + 0.1 * x * y + 0.1 * x * x + 0.1 * Math.Sqrt(Math.Abs(x)); ret += (20.0 * Math.Sin(6.0 * x * Pi) + 20.0 * Math.Sin(2.0 * x * Pi)) * 2.0 / 3.0; ret += (20.0 * Math.Sin(x * Pi) + 40.0 * Math.Sin(x / 3.0 * Pi)) * 2.0 / 3.0; ret += (150.0 * Math.Sin(x / 12.0 * Pi) + 300.0 * Math.Sin(x / 30.0 * Pi)) * 2.0 / 3.0; return ret; } public static GpsPoint Wgs84ToGcj02(double lat, double lon) { if (OutOfChina(lat, lon)) return new GpsPoint(lat, lon); double dLat = TransformLat(lon - 105.0, lat - 35.0); double dLon = TransformLon(lon - 105.0, lat - 35.0); double radLat = lat / 180.0 * Pi; double magic = Math.Sin(radLat); magic = 1 - Ee * magic * magic; double sqrtMagic = Math.Sqrt(magic); dLat = dLat * 180.0 / ((A * (1 - Ee)) / (magic * sqrtMagic) * Pi); dLon = dLon * 180.0 / (A / sqrtMagic * Math.Cos(radLat) * Pi); return new GpsPoint(lat + dLat, lon + dLon); }

逻辑说明:这套公式把经纬度当作平面量做三角函数叠加,再按参考椭球做弧度修正。代码最后返回的是原始值加偏移量,而不是直接替换,这点和网上流传的版本一致,抄的时候别改错。

参数说明:三个常量和两个边界值都来自公开实现,没有需要调的业务旋钮,唯一要注意的是别把函数名改成自己的然后传参顺序搞反。调用时机建议放在解析之后、画图之前:读到一个有效GpsFix就立刻转一次并存结果,不要在UI线程里反复转同一个点。返回值用GpsPoint包装,避免out参数带来的可读性问题。

3.3 转换对没对:固定点验证与BD-09的二次偏移

第一,拿你所在城市任何一个能在地图上准确识别的点做实验:记录模块输出的WGS-84原始坐标,转换后坐标在地图上标注出来,看点位和真实地物是否贴合。转换生效的特征是点位从飘在别处变到落回路面上,偏差收敛到几米以内。第二,用固定点观察漂移范围:GPS模块放窗台上不动,连续记录10分钟转换后的坐标,如果点聚集在一个半径几米的圆内,而不是单向偏移几十米,说明坐标系链条是通的。反过来要是所有点整齐地偏向某个方向,那基本可以断定少了一次转换。

这里还要提醒一句:部分图商的地图还提供BD-09坐标,是另一种偏移体系。如果你选的地图服务走的是BD-09,需要先做GCJ-02转BD-09的二次转换。判断依据很简单,看图商API文档里坐标系的字段名,别想当然。所有转换逻辑集中在同一个静态类里,哪个环节出错都能从日志里看出来。

4. 把定位画到地图上:WinForms与地图JS的双向通信

4.1 地图选型:WebBrowser加载图商JS还是本地瓦片控件

WinForms里画地图,常见做法有两条路。第一条是WebBrowser控件加载一个本地HTML,HTML里用某图商的JavaScript地图API,C#通过InvokeScript调用页面里的JS函数来更新标记和轨迹。这条路的优点是地图渲染、缩放、瓦片全部由图商JS搞定,代码量小,跟着开放平台的文档就能跑通,缺点是必须联网,而且要去开放平台申请Key。第二条是引用一个开源桌面地图控件,加载本地瓦片或在线瓦片,纯C#画覆盖物,完全不依赖浏览器。这条路离线能力强,但没有商业图商那样成熟的API,配置瓦片源和坐标对齐要费更多功夫,遇到显示bug得自己查。

我一般建议新手和只做内部工具的人走第一条,理由很实在:你缺的不是地图渲染引擎,而是把定位点标上去的简单交互,JS方案两小时能跑通,开源控件方案可能要连续调试两天。如果你做的场景是野外无网络环境,再回头评估桌面瓦片控件,不要把选型问题想复杂了。

4.2 本地HTML地图页:Marker复用与轨迹点截断

JS方案里HTML是承上启下的关键,C#端只负责把坐标字符串塞进来。下面的页面代码统一用MapApi作为占位符,实际接入时,把构造函数名和引入地址替换成你所选图商开放平台文档里的写法:

<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>locator</title> <!-- 引入图商JS API,需在开放平台注册应用拿Key --> <script src="https://map.example.com/js/api?v=1.0&key=YOUR_KEY"></script> <style>html, body, #map { width:100%; height:100%; margin:0; padding:0; }</style> </head> <body> <div id="map"></div> <script> var map = new MapApi.Map('map', { zoom: 15 }); var marker = null; var trackPoints = []; function updateMarker(lng, lat) { if (!marker) { marker = new MapApi.Marker({ position: [lng, lat] }); map.addOverlay(marker); } else { marker.setPosition([lng, lat]); // 复用Marker,不反复创建 } trackPoints.push([lng, lat]); drawTrack(); } function drawTrack() { var pts = trackPoints.slice(-300); // 最多保留300个点,防卡顿 if (window.trackLine) map.removeOverlay(window.trackLine); if (pts.length > 1) { window.trackLine = new MapApi.Polyline({ path: pts, strokeColor: '#FF6600' }); map.addOverlay(window.trackLine); } } </script> </body> </html>

逻辑说明:页面暴露一个全局函数updateMarker,C#侧所有更新都汇聚到这一个入口,避免C#和JS之间定义一堆来回调用的接口。轨迹线用window对象缓存,每次重画前先移除旧线,防止覆盖物无限累积。

参数说明:zoom控制在14到16之间,城区巡检够用,农村或厂区可以放到16以上。Marker一定要复用,每帧new一个,跑一天下来页面会卡到无法操作。轨迹点截断用slice(-300),这是最简单的性能保护,别小看它,全天候外勤的轨迹全量画出来,浏览器必卡。改动轨迹线颜色和宽度,在图商API的Polyline配置里改,注意strokeWeight单位是像素。

4.3 C#调用JS更新点位:线程切换与InvariantCulture参数

C#操作WebBrowser有两件事必须做对:一是把坐标更新放到UI线程,二是参数类型别搞错。InvokeScript传参时,C#的double会变成JS里的数字,没问题;怕的是直接把GpsFix对象传进去,JS那边拿到的是COM包装对象,取字段非常别扭。我的做法是只传两个字符串,在JS里再parseFloat,绕开所有类型转换的边界情况。

// GPS定位Demo:把坐标更新到地图页面 private void UpdateMap(double lat, double lon) { // DataReceived是后台线程,强制切回UI线程 if (InvokeRequired) { BeginInvoke(new Action(() => UpdateMap(lat, lon))); return; } if (webBrowser1.Document == null) return; // 页面没加载完,丢弃这一帧,等下一次1Hz更新 GpsPoint gcj = Wgs84ToGcj02(lat, lon); // 画到图商地图前做坐标转换 object[] args = { gcj.Longitude.ToString(CultureInfo.InvariantCulture), gcj.Latitude.ToString(CultureInfo.InvariantCulture) }; webBrowser1.Document.InvokeScript("updateMarker", args); }

逻辑说明:InvokeRequired判断线程,BeginInvoke把整个方法重新投递到UI线程再执行一次,这是WinForms下跨线程更新的标准写法。坐标转换放在显示这一刻做,是因为串口读出来的是WGS-84,地图显示的是GCJ-02,两者必须在这里完成换算。

参数说明:Document为空的情况要友好处理,WebBrowser从Navigate到Document可用之间存在一段空白期,直接InvokeScript会抛异常,丢弃这一帧等下一次更新即可。用InvariantCulture转字符串,是避免在中文区域设置下double.ToString把小数点变逗号,然后JS的parseFloat解析失败,这个bug非常隐蔽,翻车过不止一次。调试阶段把webBrowser1的ScriptErrorsSuppressed设为false,JS一旦报错会弹脚本错误提示,比瞎猜强得多。

5. 避坑清单:GPS定位地图开发中反复出现的5个问题

这一章把我在某定位项目里反复遇到的坑按"现象、原因、解决"整理出来。遇到问题别急着怀疑硬件,先按下面这张表排查优先级:

现象先查什么常用手段
解析一直为空定位状态位、波特率串口助手 + GPGSV卫星数
点位漂移锯齿坐标系链路固定点连续记录10分钟
地图白屏Key、网络、脚本错误浏览器直接打开HTML

5.1 串口有数据但解析一直为空

现象:串口能打开,串口助手能看到$GPRMC在跳,但程序里ParseNmeaLine一直返回null。

原因:十有八九是定位状态位问题。模块刚上电、天线平放在桌面、或者人在室内时,GPS还没完成星历捕获,GPRMC的第三个字段就是V,GPGGA的定位质量是0,代码按规则过滤掉这些无效帧,结果自然为空。剩下两成是接线问题:模块的TX要接电脑端的RX,接反或悬空会读出乱码;部分模块默认波特率不是9600而是115200,用错波特率时语句里全是乱码。

解决:先把模块挪到窗边等一分钟,观察$GPGSV里的卫星数量有没有增长到三四颗以上。调试阶段把无效帧也输出到日志,确认不是程序误判。接线用杜邦线交叉接TX和RX,通电前核对模块供电电压要求,别接反电源烧模块。

5.2 地图上点位漂移、轨迹呈锯齿状

现象:模块静止放在窗台,地图上的点却跨过好几条街跳动,轨迹像毛线团。

原因:先查坐标系链路,把WGS-84原始坐标直接送到GCJ-02地图上,点位会保持几十米的固定方向偏移,看起来像整体漂移。再看帧更新逻辑,有些模块上电后会发出历史缓存帧,与当前真实位置混在一起,每帧都更新地图就出现无规律跳动。另外,代码里同时解析GPGGA和GPRMC并各自更新,两种语句每秒交替输出,坐标微小差异叠加后就变成抖动。

解决:坐标系问题用第3章的固定点验证法,把模块放一个已知地物旁十分钟,看偏移是否是固定方向。帧混乱问题在解析侧做位移过滤:相邻两点距离超过100米且时间差小于2秒时,视为异常帧丢弃。语句只用一条,优先GPRMC,字段干净,速度信息也在同一行里。

5.3 室内和阴雨天收不到卫星

现象:晴天室外定位正常,一到室内、树荫下、高架桥底就持续没有有效定位。

原因:GPS信号本质是来自卫星的微弱微波,模块对天空视野要求很高,室内窗边的卫星数量往往只有一两颗,达不到定位下限。这个坑最容易被当成源码bug,然后白折腾一晚上。

解决:程序侧不需要额外处理,只要静默忽略无效帧,别弹错误框吓用户。硬件侧把模块装到设备顶部,或接外置有源天线,天线头朝天空。选型上优先考虑GPS加北斗的双模模块,城市峡谷场景可用性明显提升。为了调试不依赖真实环境,我习惯先录制一段室外NMEA日志,在室内用日志回放驱动程序,做法在第6章。

5.4 地图区域白屏或一直转圈

现象:WinForms窗口正常,WebBrowser区域一片白,或者地图一直转圈不出瓦片。

原因:三件事逐一排查。一是图商Key没配置,或域名白名单把file协议拦截了;二是运行电脑没能正常访问互联网,地图瓦片拉不到;三是本地HTML里外链JS引入顺序错,或者ScriptErrorsSuppressed一直是false导致脚本错误被吞掉。

解决:开发期把ScriptErrorsSuppressed设为false,页面JS一报错WinForms就弹提示,比猜快得多。然后把HTML用浏览器直接打开,排查独立页面能不能显示地图;如果file协议被拦截,把页面放到本机IIS下用http访问。最后核对运行机器网络与Key的配额,瓦片请求失败往往表现为区域白屏。

5.5 坐标转换后点位反而更偏

现象:调用了Wgs84ToGcj02之后,点反而从路面飘到空地,甚至经纬度出现与其他点明显不连续的跳变。

原因:输入坐标本身已经经过一次GCJ-02或BD-09偏移,再转一次就是二次偏移,误差被放大;另一个可能是OutOfChina边界判断误判,让国内坐标被放行不做转换。

解决:先确认数据源坐标系。有些定位设备说明书里写明输出的是图商坐标系,这种源头就要跳过转换。给转换函数前后加日志,记录原始值与转换值,连续打印几帧,拿到地图上对照,马上能看出是重复转换还是边界问题。如果地图选的是BD-09,还得补GCJ-02转BD-09这一步,别略过。

6. 从能跑到能用:轨迹平滑、定位质量与日志回放

程序能画点只算跑通,离能用还差三件事:轨迹平滑、质量过滤、日志回放。

轨迹平滑我用中值滤波,不用均值。GPS单点跳变是偶发的,均值会把跳变摊进整段轨迹,中值直接丢掉离群值:

// GPS定位Demo:5点中值滤波,平滑单点跳变 private readonly Queue<double> latBuf = new Queue<double>(); private readonly Queue<double> lonBuf = new Queue<double>(); private double Median(Queue<double> buf, double value, int window) { buf.Enqueue(value); if (buf.Count > window) buf.Dequeue(); double[] arr = buf.OrderBy(v => v).ToArray(); return arr[arr.Length / 2]; }

逻辑说明:纬度和经度各维护一个队列,分别滤波,窗口取5。注意这个滤波器要作用在解析后、坐标转换前的原始坐标上,因为GCJ-02转换本身含非线性项,先滤波再转换更合理。

参数说明:window取5到7之间,数值再大轨迹会显得迟钝。滤波只处理小幅度抖动,大幅度跨街跳变要靠第5章的速度位移过滤在解析层干掉,两层各管各的。

定位质量方面,解析GPGGA时把HDOP字段一起带出来,HDOP小于5的点正常绘制,大于10的点标记为低精度或干脆不画。固定点测试时HDOP的波动比速度更能提前暴露信号劣化,程序里留一个日志字段记录它,后面做报表很有用。

日志回放是我现在调试定位程序最依赖的手段:把所有原始NMEA语句原样追加到本地文件,调试时用Timer按真实间隔逐行喂给同一个解析函数,这样在室内也能复现室外的轨迹。改一行代码,回放一遍日志,把界面刷新问题从硬件依赖里彻底解耦出来。我的习惯是串口事件里先落盘再解析,所有坐标问题都靠日志回放定位,而不是蹲在窗台边等卫星。这套源码跑通之后,建议你也保留这个习惯,希望帮到你。

本文还有配套的精品资源,点击获取

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

408错题整理与复盘全攻略:从工具选型到周期回顾

备考 408 的同学&#xff0c;十有八九都会准备一个错题本。但很多人整理错题的方式&#xff0c;其实停留在“把题目抄下来、把答案记在旁边”这一步&#xff0c;等第二遍看的时候&#xff0c;既不理解当初为什么错&#xff0c;也不知道这道题对应的知识点到底哪里没通。本文围绕…

作者头像 李华
网站建设 2026/10/12 2:41:06

ROS2与Autoware从仿真到实车部署全流程避坑指南

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

作者头像 李华
网站建设 2026/10/12 2:40:33

商业航天启示:学可回收火箭模式为何极难走通?

为什么全世界都在学R公司&#xff0c;真正走通的却屈指可数&#xff1f;这个问题我从入行起就一直在琢磨。这里用R公司代指那家凭借可回收火箭把发射成本拉到行业新低、又用低轨通信星座建立自我造血闭环的头部商业航天企业。我不太想聊某家公司的光环&#xff0c;而是想聊它背…

作者头像 李华
网站建设 2026/10/12 2:40:06

HP DL380 G6/G7 P410i阵列卡驱动加载与RAID识别实战指南

简介&#xff1a;本资源为惠普HP DL380 G6/G7服务器专用P410i智能阵列控制器官方驱动合集&#xff0c;面向企业IT运维人员、服务器管理员及硬件维护工程师&#xff0c;专用于解决RAID磁盘无法识别、系统安装失败或存储性能异常等典型问题。压缩包共11个文件&#xff0c;含核心驱…

作者头像 李华
网站建设 2026/10/12 2:40:04

原生HTML手写可删除Tab多窗口:从结构到状态管理

简介&#xff1a;这是一份面向前端初学者与页面交互开发者的HTML标签页组件示例&#xff0c;聚焦“多窗口切换可删除”这一常见交互需求。资源以Bootstrap的nav-tabs与tab-content为基础搭建导航与面板结构&#xff0c;再借助jQuery为每个标签补充删除按钮&#xff0c;点击后同…

作者头像 李华
网站建设 2026/10/12 2:39:33

机器学习图像分类实战:HOG特征+SVM传统方案详解

简介&#xff1a;面向机器学习初学者、研究人员与开发者的图像分类学习项目包&#xff0c;集成支持向量机与贝叶斯分类器&#xff0c;并通过图形界面让用户直接加载图像、提取特征、对比分类结果&#xff0c;省去命令行配置的繁琐。压缩包为RAR格式&#xff0c;共216个文件&…

作者头像 李华