Quectel DIAG 记录契约:7E 分帧、双层长度与时间语义

作者:

在

结论:Quectel DIAG 接收链的真实风险不在「能不能解出卫星数」,而在三处宽松的输入契约:AT 文本用子串判断状态、7E 分帧没有单帧长度上限与结束符超时、内外两层长度校验只保证封装边界而不保证 payload 与版本/星数匹配。这些缺口都不会让进程崩溃,只会让上层在「日志仍在发布」时误判。以下基于固定源码快照 c8fb906 的静态阅读与隔离探针,未打开 AT/DIAG 端口、未写 NV、未做实板定位测试。

从 AT 端口到 DIAG 日志:初始化顺序与文本解析

链路入口先等 AT 端口。qcomgpsd.main() 创建 ModemDiag 并调用 setup_quectel();AT 命令通过 at_cmd() 锁住 /dev/shm/modem.lock,与 modem、LPA 共享所有权。at_cmd() 先取进程间锁再打开串口,写入以 CR 结尾的命令、丢弃回显并返回数据行。DIAG 串口是 /dev/ttyUSB0,以 115200 波特率、RTS/CTS 与 DSR/DTR 硬件流控独占打开。初始化会依次请求 NV 写/读、日志掩码、可选关闭已有 GNSS、DPO/自启配置、可选辅助时间、NMEA 输出、GNSS 启动与 OEMDRE 命令;外层 main() 在 setup_quectel() 返回后才初始化并拉高 GNSS_PWR_EN,信号清理则先拉低该脚再调用 teardown_quectel()。

这一层的文本解析相当宽松:ERROR 与 +CME ERROR 只作为终止行,不会转成异常;若没有数据行,函数会像收到 OK 一样返回空字符串。wait_for_modem() 只要求端口路径出现并在 AT 文本中找到 +QGPS:,因此它接受 +QGPS: 0 的关闭状态,也没有整体 deadline;gps_enabled() 用 "QGPS: 1" in response 判断,连 +QGPS: 10 都会返回真。这些是文本解析语义,不代表真实模组会给出该状态码。

重试也是嵌套的:try_setup_logs() 的十次重试嵌在 setup_quectel() 的五次重试内。隔离探针用始终失败的假 setup_logs() 观察到 50 次调用与 55 次一秒睡眠,并且每轮外层重试前都会重复发送 NV 写/读请求。它证明的只是主机调用顺序与软件重试上限,真实串口阻塞、NV 写入效果与总耗时都未验证;两层通用异常还会替换故障报告,无法从最终异常反推底层 DIAG 失败原因。

层次 字段/机制 核对内容
分帧 7E / 7D / CRC16 发送附 CRC16/X-25,转义 7D/7E 并以 7E 结束;解码用断言检查结尾与 CRC
外层封装 <BH> 外层长度须等于内层字节数
内层封装 <HHQ> 内层长度须等于内层总字节数,并取出 log_type 与模组 log_time

7E 分帧与一个没有上限的接收循环

接收端 recv() 以结束符从串口块拼接,并把未配对部分留在 pend,hdlc_decapsulate() 解转义后用断言核对结尾和 CRC。真正的问题是 recv() 没有单帧最大长度,也没有结束符等待超时:连续不含 7E 的输入会持续累积在局部列表中,脚本无法从中判断它是「正在等一帧」还是「已经失控积压」。

同样要注意容错的边界:CRC、短帧、外层长度或日志版本异常没有在 main() 循环内转成「坏帧计数并继续」,多数会抛出异常或断言,需要由进程监督重启。send_recv() 在等待命令应答时会跳过遇到的 DIAG_LOG_F,所以设置/清理日志期间不能假定每个测量都能被发布。另有一处不对称:setup_logs() 对首个「查询日志 ID 范围」响应只解析内层 operation/status,没有检查外层 opcode,而后续逐类别设置 mask 的响应则显式断言 opcode;这证明的是主机解析门缺失,而非模组会发出这种交错回包。

三条测量记录的字节边界

DIAG 帧取到操作码后只处理 DIAG_LOG_F,随后用 parse_struct() 把声明字符串转成无隐式对齐的小端 struct 格式;名字含 _Flt 时即使声明为 uint32 也按 IEEE-754 float 读,含 _Dbl 则按 double 读。dict_unpacker() 使用 unpack_from,只要求输入长度至少为 schema 大小,不校验恰好耗尽;因此插件若沿用 C 结构体的默认 padding,或只看类型名,会错读浮点与后续字段。

记录 头部 / 单星长度 主循环中的检查与边界
OEMDRE 测量 95 / 109 字节 检查版本 2,按 svCount 切片并逐星 unpack_from;切片前不验证 95+109*svCount == payload_len,短片抛解包异常、尾随字节可被忽略
GPS 测量 28 / 70 字节 检查版本 0;svCount>0 时用 len(sats)//svCount == 70(非精确总长度,两星多 1 字节仍通过);svCount==0 时不检查剩余长度
GLONASS 测量 29 / 70 字节 与 GPS 共用星数检查,两者状态位扩展不同

上述长度来自对 dict_unpacker() 的纯 Python 探针:OEMDRE (95,109)、GPS (28,70)、GLONASS (29,70);同一探针确认头部末尾额外 1 字节仍可解出字典,且 (2*70+1)//2 == 70。这证明了解码器的具体边界行为,但不证明模组实际会发送异常帧。

时间、定位与有效性语义

qcomGnss 的 union 承载 DR 测量、SV 多项式与 GPS/GLONASS 测量等。主循环把 DIAG 内层的 log_time 原样写入 logTs,没有在此处转换成 Unix 或 Linux 单调纳秒;发布消息的 logMonoTime 是消息构造时间,不能替代模组的测量时间。

gpsLocation 映射只接受 u_PosSource==2 且 GPS week 非 0xFFFF 的位置报告,把弧度经纬/航向转成角度、ENU 速度变为 NED;hasFix 以 verticalAccuracy != 500 判定,而该字段取自 q_FltVdop(垂直精度因子),能否当作以米计的垂直精度需要模组协议与实测核对,不能靠字段名推断。Unix 时间由 GPS 周与周内毫秒减固定 18 秒生成,随后由 timed 根据定位主题的新鲜度、hasFix 与时间范围决定是否校时。

常见问题

DIAG 接收为什么会持续积压?

因为 recv() 没有单帧最大长度,也没有结束符等待超时;连续不含 7E 的输入会持续累积在局部列表中,脚本无法区分「正在等一帧」与「已经失控积压」。

外层与内层长度校验通过就说明 payload 匹配吗?

不是。两层长度只保证封装边界,不保证该类型 payload 与版本/星数匹配。OEMDRE 在切片前不验证 95+109*svCount == payload_len,尾随字节可被忽略。

GPS 测量两颗星时多 1 字节为什么仍能通过?

因为主循环用的是 len(sats)//svCount == 70,而不是精确总长度校验;整数除法让多出来的 1 字节被容忍,svCount==0 时甚至不检查剩余长度。

verticalAccuracy 能当作以米计的垂直精度吗?

不能直接这样用。它取自 q_FltVdop,其声明注释是垂直精度因子;是否等于以米计的垂直精度,需要模组协议与实测核对,不能仅凭 hasFix 的判定条件推断。

延伸阅读

参考资料

  • 项目 02 —— openpilot-research/GNSS_QUECTEL_DIAG_RECORD_CONTRACT_DEEP_DIVE.md(AT/DIAG 调用链、7E 分帧与三条记录的字节边界,含源码行号索引)
  • 项目 02 —— openpilot-research/probes/test_quectel_modemdiag_contract.py(CRC16/X-25、转义往返、分块保留与 send_recv() 跳帧六项探针)
  • 项目 02 —— openpilot-research/probes/test_quectel_nested_setup_retry_contract.py(内层十次嵌套外层五次的调用次数与睡眠统计)

说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

更新记录

首次发布 2026-10-11 最近核对:2026-10-11

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注