做室外机器人定位这件事,绕不开GNSS。我最近在Jetson Orin NX 16GB上接了一个联适R70M-GNSS模块,目标很明确:用串口把高精度定位数据读出来,并解析成程序能直接用的经纬度和状态信息。这套组合不是什么高深技术,但正因为基础,踩坑的人反而特别多——电平接反、串口被占用、RTK一直不固定、数据丢一半……这篇文章把你可能遇到的问题都梳理一遍,并给出可直接复制的代码和命令。适合正在做机器人、自动驾驶、测绘设备集成的朋友,尤其是刚把Jetson和GNSS模块凑到一起、却不知道从哪下手的阶段。
1. 项目整体设计与思路拆解
1.1 为什么是 Jetson Orin NX 与 R70M-GNSS 的组合
Jetson Orin NX 16GB 是边缘计算里很常见的一块板卡,16GB的统一内存意味着可以同时跑检测、分割这类视觉模型和定位导航逻辑。而联适R70M-GNSS不是普通的GPS模块,它支持RTK/PPP这类高精度解算,既能输出GPS/北斗等系统的卫星定位数据,也能输出原始观测数据和差分数据。把两者接在一起,本质上是给Jetson加了一个“厘米级”的室外定位源。对做无人配送车、巡检机器人、农业机械导航的朋友来说,这个搭配属于非常标准的“计算平台 + 高精度定位模块”组合。
串口在这个项目里承担了唯一的桥梁角色。为什么不用USB或网口?因为R70M这种专业GNSS接收机通常都会提供UART接口,在嵌入式系统里,UART最少只需要三根线,而且只要波特率和数据格式设置正确,就能稳定接收数据。对Jetson来说,Linux把串口设备抽象成了 /dev/ttyUSB0 这类文件,open/read就行,不用处理复杂的协议栈。这个选择最省事,也最容易排查问题:先看线,再看权限,最后看软件解析,每一环都很直观。
1.2 串口是GNSS数据流的“最后一公里”
GNSS数据从卫星到应用要经历好几跳:天线先接收射频信号,接收机内部完成捕获、跟踪、解调、解算,最后把结果按照约定协议推出来。协议层面最常见的就是NMEA 0183,它是一行行的ASCII文本,以“$”开头,语句之间用回车换行隔开。R70M在默认配置下就是通过串口输出这些语句,所以只要把串口数据流接到Jetson,就已经走通了数据链路的最后一公里。
这一公里虽然短,却最值得花时间打磨。因为后面的建图、导航、时间同步全都依赖这一行数据是否完整、及时。如果程序读串口时处理不过来,或者系统把GPS信息当成普通串口日志混在一起,那么后面所有轨迹都会飘。我的习惯是先把串口数据流当成一个独立的“数据源”,专门用一个线程或进程去读,解析结果通过队列交给上层,而不是在主流程里顺手读两行。这也是后面第3章、第6章代码模板的核心思想。
补充一个选型心得:为什么选“16GB”而不是“8GB”?对我来说,16GB版本在跑视觉模型时基本不需要担心内存溢出,剩下的显存还能给RTK解算辅助或SLAM留余量。如果你只做定位数据采集,8GB也够,但做自动驾驶或完整机器人系统,16GB明显更从容。
2. 硬件接线与系统环境准备
2.1 硬件清单与接线要点
在开始敲命令之前,先确认手里的硬件。我这边清单如下:
- Jetson Orin NX 16GB开发套件(载板是第三方定制,40Pin引脚和供电方式略有差异)
- 联适R70M-GNSS接收模块(我这里用的是TTL电平UART版本)
- 配套有源天线(必须用GPS/北斗双频天线,插上去拧紧)
- USB转TTL适配器(CH340或CP2102都行,用来看串口数据)
- 杜邦线
- RTK差分源(可选,通过内置4G网络模块或外接电台)
接线顺序我是这样做的:先把GNSS模块断电,然后接三根线:模块TXD → Jetson载板或者USB转TTL的RXD,模块RXD → Jetson的TXD,最后GND共地。这个顺序看起来简单,但最容易错的恰恰是TX/RX交叉。如果你发现数据完全收不到,第一步就该怀疑是不是两条数据线接反了。
还有一个容易忽略的问题是电平:Jetson 40Pin上的UART一般是1.8V或3.3V逻辑,而R70M如果默认是TTL 3.3V,可以直接对接;如果是RS232电平,就不能直接接,必须加一个RS232转TTL模块。我一开始差点踩了这个坑,因为联适有的板卡型号会选配RS232口。大家接线前务必看一眼模块铭牌或手册,确认是“UART TTL”还是“RS232”。USB转TTL适配器则基本都带电平转换,相对安全。
天线也要提前规划。GNSS天线必须放在开阔位置,最好车顶或窗边,不要被金属遮挡。R70M模块的供电一般是3.3V-5V,我直接从载板取5V,注意电流别超过手册限值。最后插上USB转TTL到Jetson USB口。你可能会有疑问:Jetson不是有板载UART吗,为什么还要额外买USB转TTL?因为Jetson的40Pin串口默认可能被控制台占用,需要改设备树或通过config去启用,新手容易卡住。用USB转TTL适配器则即插即用,系统自带CH340/CP2102驱动,所以我推荐先用USB转TTL跑通流程,之后再考虑板载UART以节省USB口。
2.2 Jetson串口驱动与设备识别
Jetson默认系统是Ubuntu,大多数USB转串口芯片的驱动已经编译进内核。插上USB转TTL后,先执行 lsusb 和 dmesg | tail,看内核是否识别到了设备。如果芯片是CH340,lsusb里会出现 QinHeng Electronics HL-340 serial converter 这类字样;CP2102则显示 Silicon Labs CP210x。如果没识别,最直接的办法是换一根USB线或者重插一次,有时候供电不足也会导致设备反复掉线。
识别成功后,设备节点通常叫 /dev/ttyUSB0。如果同时挂了多个USB串口,会按顺序编号 ttyUSB0、ttyUSB1。为了不认错设备,建议用 udev 固定一个符号链接。我在 /etc/udev/rules.d/99-gnss.rules 里写了这样一行:
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", MODE="0666", SYMLINK+="gnss"这里的 idVendor 和 idProduct 要根据 lsusb 查到的实际值改。CH340一般是 1a86:7523,CP2102是 10c4:ea60。写好后执行 sudo udevadm control --reload-rules,重新拔插USB,之后 /dev/gnss 就会指向同一个设备号,权限也变成了0666。
如果你非要用板载UART,那需要确认Jetson的40Pin串口是否被系统占用。默认情况下,有的镜像会把某个UART配置成调试串口,直接用它会导致乱码或冲突。我建议第一版先用USB转串口跑通,板载UART作为优化项再处理。另外,无论用哪种,当前用户都要加入 dialout 组,否则读写 /dev/ttyUSB0 可能报 Permission denied:
sudo usermod -aG dialout $USER改完组之后要重新登录一次才生效。这一步虽然简单,但很多新手卡在“明明有设备却打不开”上,其实就是没加组。
3. 串口读取定位数据的四种实操方式
3.1 先用 minicom 粗看数据
拿到定位数据之前,不要直接写代码。先用现成工具验证链路通不通,省得后面调试时分不清是硬件还是软件问题。minicom 是Linux下很常用的串口终端,安装就一行命令:
sudo apt install minicom然后启动:
minicom -D /dev/ttyUSB0 -b 115200打开后如果一切正常,屏幕上会不断滚动类似这些文本:
$GPGGA,082756.00,3117.30845,N,12128.41482,E,1,08,1.0,7.8,M,-3.2,M,,*5B $GPRMC,082756.00,A,3117.30845,N,12128.41482,E,0.5,123.4,230225,3.2,W*6F看到 $GPGGA、$GPRMC 这行就说明串口通信已经通了。没看到就检查接线和波特率。minicom退出快捷键是 Ctrl+A 然后 Q,它会直接问你是否退出,选 Yes 就行。
这里我要说一个习惯:看到NMEA语句后,先不要急着关掉,观察几秒钟,看行与行之间是否连续、有没有半行乱码。如果连续滚动但偶发乱码,往往是波特率或电平问题;如果完全静止,则是TX/RX接反或者模块没在输出。这一步虽然简单,却能帮你把问题范围缩小一大半。我在调试时还会顺便看下卫星数,如果一直是0,那就说明天线没搜到星,先别折腾串口了。
3.2 用 Python pyserial 按行读取
确认链路正常后,就可以写读取脚本了。Python 的 pyserial 是处理串口的标准库,安装:
pip install pyserial一个最小读取脚本如下:
import serial ser = serial.Serial( port='/dev/ttyUSB0', baudrate=115200, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=1.0 ) while True: line = ser.readline() if line: text = line.decode('ascii', errors='ignore').strip() if text.startswith('$'): print(text)这段代码的作用是:打开串口、按行读取、解码成ASCII字符串,然后打印。timeout=1.0 让 readline 最多等一秒,避免程序卡死。实际使用中还要处理异常——串口拔掉、进程被杀、系统休眠回来之后,串口对象会失效。我习惯把打开和读写封装成类,重连时重新调用 serial.Serial,这点在第6章会给出更完整的模板。
串口读数据看似简单,但有一个细节必须注意:串口读操作默认是阻塞的,如果上层处理速度跟不上GNSS数据帧速率,数据就会积压在系统缓冲区里,读出来时可能已经丢帧。GNSS输出频率通常不高(5Hz或10Hz),每条NMEA语句也就几十个字节,所以Python按行读一般不会丢。但如果你同时还在做图像推理、把读串口放到主线程里,就有风险。最好单独开一个线程专门读。
3.3 解析经纬度并保存到文件
打印NMEA仅是第一步,真正有用的是把经纬度、高度、定位状态解析成结构化数据。最简单的方式是用 pynmea2 库:
pip install pynmea2然后写个脚本,只提取 GGA 消息中的关键字段:
import serial import pynmea2 ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=1.0) while True: line = ser.readline() try: text = line.decode('ascii', errors='ignore').strip() if text.startswith('$GPGGA'): msg = pynmea2.parse(text) print(f"时间 {msg.timestamp} " f"纬度 {msg.latitude} {msg.lat_dir} " f"经度 {msg.longitude} {msg.lon_dir} " f"质量 {msg.gps_qual} " f"卫星数 {msg.num_sats} " f"海拔 {msg.altitude} m") except Exception as e: print("解析失败", e)pynmea2 自动把 “3117.30845,N” 这样的度分格式转成十进制度数,非常省事。想保存历史轨迹,可以直接把解析结果追加到CSV:
import csv from datetime import datetime with open('gnss_track.csv', 'a', newline='') as f: writer = csv.writer(f) writer.writerow([datetime.now().isoformat(), msg.latitude, msg.longitude, msg.altitude])不过要注意,日志里最好同时记录原始NMEA语句。因为CSV一旦字段定义错了,后面换一种消息类型再补就麻烦了。我实际做采集时,都是原始报文和解析结果各存一份,原始报文用于校验,解析结果用于实时展示。这样一旦最终轨迹出现问题,还能回到原始数据重新查。
3.4 用 socat 做数据分发,解决多进程抢串口
很多场景下你不可能只用一个进程读串口。比如我想一边在终端看NMEA报文、一边让ROS2节点解析定位,两边同时读同一个 /dev/ttyUSB0 会冲突,因为Linux串口默认只允许一个进程独占。这时可以借 socat 把物理串口“映射”到TCP端口,让多个进程分别作为客户端连接。socat 是一个数据转发工具,安装 apt install socat,然后:
socat -d -d /dev/ttyUSB0,raw,echo=0,b115200 TCP-LISTEN:54321,reuseaddr这条命令会把 /dev/ttyUSB0 收到的数据转发到 TCP 54321 端口,其他程序可以分别用 nc 连接看数据,也可以在自己的代码里用 socket 读取。也可以反过来,把TCP端口收到的数据写进串口,这就成了双向桥接。
socat 方案虽然好用,但初次配置容易把链路搞混。TCP端口同时允许多个连接,所以比伪终端的“多进程读”更直观。我建议先用第3.2节单进程脚本验证核心功能,再考虑分发。如果只是临时想开两个终端看同一份数据,socat 是最省事的。
4. 数据解析与 RTK 定位细节
4.1 NMEA 0183 常用语句拆解
NMEA 0183 是GNSS接收机最通用的输出协议,每一行都是一个句子,以 $ 开头,以回车换行结束。我们最常用的两句话是 $GPGGA 和 $GPRMC。专门做高精度定位时还会看 $GPGSV、$GNGGA 等。R70M支持多系统联合定位,所以很多报文以 “GN” 开头,比如 $GNGGA,含义和 GPGGA 基本一致,只是兼容了GPS+北斗+GLONASS。
GGA语句字段举例:
$GNGGA,082756.00,3117.30845,N,12128.41482,E,1,08,1.0,7.8,M,-3.2,M,,*5B
| 字段 | 含义 | 示例 |
|---|---|---|
| 082756.00 | UTC时间(时分秒) | 08:27:56.00 |
| 3117.30845 | 纬度,度分格式 | 31度17.30845分 |
| N | 北纬/南纬 | N |
| 12128.41482 | 经度,度分格式 | 121度28.41482分 |
| E | 东经/西经 | E |
| 1 | 定位质量:0无效,1单点,2差分,4RTK固定,5RTK浮点 | 1 |
| 08 | 参与解算的卫星数 | 8 |
| 1.0 | HDOP水平精度因子 | 1.0 |
| 7.8,M | 海拔高度和单位 | 7.8米 |
| -3.2,M | 大地水准面差距和单位 | -3.2米 |
记住一个关键点:度分格式不是十进制度。3117.30845 表示 31度17.30845分,转十进制度就是 31 + 17.30845/60 = 31.28847417。pynmea2 已经做了转换,但如果你自己手写解析器,千万别忘了除以60。这是新手最容易算错的地方,也是第6章我建议单独写一个转换函数做单元测试的原因。
4.2 RTK初始化与固定状态判断
R70M的价值是能达到RTK固定解的厘米级精度。普通单点定位精度在1.5到2米左右,RTK固定后可以到2厘米左右,差异非常大。但RTK需要差分源:要么通过内置4G模块连接CORS或网络RTK服务,要么外接电台接收基准站差分数据。在串口上,我们需要判断当前是不是真的固定解。
判断方法有两个:第一,看GGA语句中的定位质量字段。质量=4表示RTK固定解,5表示浮点解,2表示差分但未固定。第二,看卫星数和HDOP。一般RTK固定时卫星数不少、HDOP很小,如果HDOP大于3,说明天空可见条件很差,即使有差分也很难固定。我实际操作时,先看一眼屏幕上的 $GNGGA,如果质量字段是4,就放心用;如果是5或2,得检查天线遮挡和差分网络。
R70M的RTK模式配置一般通过专用配置工具或串口指令,具体指令要按官方手册来。我这边用的是默认配置,上电后自动通过4G网络接入CORS服务,所以只要天线位置开阔,过一分钟左右就能看到质量字段跳到4。如果你的模块没有内置网络模块,就需要自己接电台或者通过串口把RTCM数据灌给R70M,这样接收机会把RTCM当作差分输入。无论哪种方式,串口上看到的最终效果都是质量字段的变化,所以先把串口数据读好,永远是第一步。
4.3 波特率、校验位等参数怎么定
串口通信参数无非就是波特率、数据位、校验位、停止位。GNSS模块最常见的默认配置是“115200 8N1”:波特率115200,8个数据位,无校验,1个停止位。这符合串口调试助手里最常见的“115200 8N1”选项,也适配绝大多数USB转TTL适配器。
为什么NMEA输出不需要很高的波特率?因为一条典型GGA语句大约70个字符,即使10Hz输出,1秒钟也就700字节,115200bps远够用。但如果要输出RTCM差分数据、原始观测数据或者双天线航向数据,数据量会暴涨,这时波特率可能要设到460800甚至921600。我实际上在采集原始观测量时,就把R70M的串口波特率调到了460800,否则会明显丢数据。
另一个值得提醒的点是奇偶校验。有些人习惯把串口调试助手的“无校验”改成“偶校验”,但GNSS模块默认通常是None,两边必须完全一致。如果解析结果是乱码或直接没有数据,先对照两边的波特率、数据位、停止位、校验位这四个参数,一字不差才能通。串口调试时不要乱猜,先用stty或minicom把参数固化下来,再动代码。
5. 常见问题、坑与排查实录
5.1 设备节点时有时无,权限拒绝
这类问题多出现在USB转串口上。现象是插上设备后 ls /dev/ttyUSB0 存在,但过一会又消失,或者程序打开时报 Permission denied。
排查步骤我总结成三个:第一步,看 dmesg | tail,确认USB设备有没有发生“disconnect”或“cannot enumerate”之类的报告。如果出现,大概率是供电不稳或USB线质量差。Jetson的USB口供电能力有限,外接有源天线不要用USB口直接供电,要给天线独立供电。第二步,看lsusb里芯片还在不在,如果在就说明内核驱动OK,问题只是设备节点;如果不在,排除线材和接口。第三步,权限问题,确认当前用户是否在 dialout 组,或者直接用代码里面的串口路径权限。如果只是临时调试,也可以 sudo chmod 666 /dev/ttyUSB0,但重启后会失效,所以最好用udev规则固化。
我遇到过一次比较隐蔽的问题:Jetson Orin NX的载板上有两个USB口,靠近电源的那个口给GNSS模块供电时电流不够,导致设备每隔几分钟就掉线一次。换成扩展坞上的另一个USB口(供电更强的)之后,连续跑了一周没掉线。所以“设备节点时有时无”时,优先怀疑供电,而不是驱动版本。
5.2 读到乱码或只有一半数据
乱码和半行数据,90%是三项原因:波特率不对、TX/RX接反、电平不匹配。波特率不对时你会看到类似 “�GPGGA…” 或不连续的字符;TX/RX接反则完全没有输出;电平不匹配可能导致要么没数据要么偶发错位。把这三个逐一排除后,通常就好了。
另外一个容易被忽略的是“多个程序同时占用了串口”。串口设备同一时刻只能由一个进程打开(除非用了socat/TCP转发)。如果之前开着 minicom,再运行Python脚本就会打开失败或读取到半个缓存。我个人的经验:每次调试前先 sudo lsof /dev/ttyUSB0 看看是谁占着串口,然后在minicom里正常退出,不要直接关终端。否则下一次打开时内核缓冲区里残留的半行数据也会被读出来。
Linux下串口数据丢失还有一个常见原因:程序读取不够快。如果主线程同时做图像处理,串口线程被调度延迟,内核tty缓冲区一旦堆满就会直接丢掉新数据。解决办法是提高读线程优先级,或者把串口数据转发到一个内存队列中,让算法线程去消费队列,而不是直接读串口。至于“串口DMA”这个概念,在嵌入式MCU上很常用,但Linux主机侧我们依赖内核驱动和 tty 层缓冲,不需要自己管理DMA,这也是为什么Jetson上和单片机写串口驱动的思路完全不一样。
5.3 天线架设与定位精度的影响
天线是所有GNSS系统里最容易压制性能的环节。很多朋友买了高精度模块,第一次上电,放在室内窗口,然后抱怨“为什么一直单点解”。GNSS信号来自卫星,必须直接接收到来自天空的电磁波,任何遮挡、反射都会造成多径效应,让定位精度急剧下降。R70M配套的有源天线更加明显,它需要供电(一般通过同轴馈线内的偏置电压供电),如果馈线接触不良,天线放大器不工作,卫星数会直接跌到0。
我通常用GGA语句中的卫星数和HDOP作为信号质量指标。如果室内能搜到6颗星,已经算不错,但RTK固定几乎不可能。到了室外开阔地,搜星数量一般在20颗以上,HDOP小于1,这时候RTK固定才稳定。天线尽量远离金属栏杆、屋顶边缘、大树,安装高度越高越好。如果设备装在移动机器人上,天线固定在车顶中央,并确保周围不被相机支架或传感器遮挡。实际测试时,我把天线从桌面移到车顶,固定解的时间从十几分钟缩短到几十秒,差距就是这么明显。
5.4 如何接入ROS2 humble / 与IMU融合建图定位
很多机器人项目最终要把定位数据接进ROS2。最常见的方式是使用 nmea_navsat_driver 包,它本质上是订阅串口或NMEA话题,然后发布 NavSatFix 消息。在ROS2 humble下可以这样启动:
ros2 run nmea_navsat_driver nmea_serial_driver --ros-args \ -p port:=/dev/ttyUSB0 \ -p baud:=115200 \ -p frame_id:=gnss_link发布出来的 /fix 话题就是标准 GPS 消息,可以给 robot_localization 做传感器融合。如果嫌这个包老,也可以自己写一个ROS2节点,在Python里调用pyserial,然后把解析结果包装成 sensor_msgs/msg/NavSatFix 发出去。串口部分和单机脚本完全一样,只是加了一个 rclpy 初始化。
和IMU融合是很自然的下一步。GNSS输出频率一般只有10Hz,IMU却能到几百Hz,两者卡尔曼融合后才能支撑高速运动的车辆姿态和位置估算。Jetson上跑 robot_localization 的 ekf_node,输入两个话题:/fix(GNSS)和 /imu/data(IMU),输出 /odometry/filtered。在做室外建图时,这个融合结果可以和激光雷达里程计互相校正,避免长时间漂移。所以现在费心把串口数据接干净,后面融合才有好原料。如果你之前用esp32或者单片机做过串口桥接,会明显感觉到Jetson这边更像一台Linux服务器,串口只是众多传感器接口中的一个,前面加一层网络转发也不是什么难事。
6. 我的实操心得与后续扩展
6.1 一个低成本稳定的串口读取脚本模板
最后分享一个可以直接拿去用的Python模块。它比前面demo多了重连和线程,能应对USB临时掉线、Jetson休眠等情况。代码结构很简单:
import serial import threading import time import pynmea2 class GNSSReader: def __init__(self, port='/dev/ttyUSB0', baud=115200): self.port = port self.baud = baud self.ser = None self.running = False self.lock = threading.Lock() self.last_fix = None def _open(self): self.ser = serial.Serial(self.port, self.baud, timeout=1.0) def read_loop(self): while self.running: if self.ser is None: try: self._open() except serial.SerialException: time.sleep(1) continue try: line = self.ser.readline() if not line: continue text = line.decode('ascii', errors='ignore').strip() if text.startswith('$GNGGA'): msg = pynmea2.parse(text) with self.lock: self.last_fix = msg except serial.SerialException: print("串口断开,正在重连...") self.ser.close() self.ser = None except Exception as e: print("解析失败:", e) def start(self): self.running = True threading.Thread(target=self.read_loop, daemon=True).start() def stop(self): self.running = False if self.ser: self.ser.close() if __name__ == '__main__': gnss = GNSSReader() gnss.start() while True: time.sleep(1) with gnss.lock: if gnss.last_fix: print(gnss.last_fix.latitude, gnss.last_fix.longitude, gnss.last_fix.altitude, gnss.last_fix.gps_qual)这个脚本我在Jetson上实际跑过,配合R70M 115200波特率输出,连续一整天没有丢包。核心思想是“串口随时可能坏”,所有读操作都在后台线程里,上层任何时候去读 last_fix 都能拿到最新的有效定位,不会因为瞬时串口错误影响整个程序。如果你要把它接进ROS2,只需要在这个线程里额外发布NavSatFix消息就行。
6.2 后续可以怎么扩展
读完定位数据只是第一步,后续可以扩展的方向很多。如果你的机器人需要在室外自主导航,可以把解析出来的经纬度换算到UTM坐标系或本地笛卡尔坐标,再和IMU输出一起喂给扩展卡尔曼滤波器。如果做测绘,可以定时保存原始NMEA和RTCM数据用于事后差分。如果想获得更平滑的轨迹,可以把双天线航向、轮式里程计都加进来。
我个人最推荐的一个方向是:先花一天时间把串口数据的记录回放框架搭好。原始报文落盘、解析字段落盘、再写一个回放脚本,这样后续调导航算法时不用每次都把设备搬出去,直接回放数据就能复现环境。很多团队把精力全花在算法模型上,反而忽略了定位数据样本的采集与回放,结果现场出了问题没法离线复现。把GNSS串口数据当作一种“信号”去认真记录,才会发现里面藏着很多调试线索。
最后分享一个小技巧:不要直接拿十进制度和角度去调坐标转换,先在脚本里打印原始语句,把度分转换为十进制的逻辑单独写成一个纯函数,并准备几组“已知经纬度”做单元测试。因为GNSS数据链路上,协议解析阶段的错误是最难查的,一旦经纬度算错,后面所有定位、建图、控制都会跟着错。串口拿到数据只是开始,把数据变成可靠、可追溯、可回放的定位结果,才是这套硬件组合真正发挥价值的地方。