Skip to content

闸机距离:全链路交付与可观测体系 ​

智能底盘策略需要「自车前方最近闸机的距离」一个 float。我要做的是从 SD/AP 双路 20Hz 感知整包里把它挑出来,5Hz 限流后跨 14 跳喂给 framework——代码本身不复杂,复杂的是上线后怎么证明每一跳都没问题,以及出了事怎么在百万行日志里快速定界。

单帧处理耗时20μs / P95 30μs
翻转响应延迟≤50ms
日志提取压缩218MB → 412KB

背景 ​

智驾静态感知的 over-all 整包(行车 SD / 泊车 AP,各 20Hz)里已经包含闸机(barrier gate)对象。需求很「轻」:新增一个信号 SIGNAL_X3D_XPU_BARRIER_GATE_DISTANCE(130102,Float,单位米),把 FOV 内最近闸机的平面距离喂给智能底盘。骨架完全复用同链路上已有的 SmartAsHandler(井盖/减速带)。设计本质是一句话:从一坨 20Hz 几十种字段的整包里,双源合并挑出「FOV 内最近闸机距离」一个 float,5Hz 喂下去。所有复杂度都在「挑」和「节流」上。

问题 ​

链路跨三种语言、四个进程边界,整整 14 跳:native C++ handler 解析 → SignalDispatcher 分发 → JniTransport 白名单 → Java DataCenter 的 SharedFlow → BarrierGateEvent 订阅 → CarIntelligentHelper → framework Car SDK。历史经验里最容易漏的两处恰恰是跨语言边界(JNI 白名单没加新信号,Java 永远收不到)和跨模块边界(helper 寻址失败,收到也调不出去)。

另一个问题是可观测。20Hz 输入端禁止全量打日志(打满就看不到别的了),但现场出问题时又必须能回答「这一刻上游有没有给闸机、卡在哪一跳」。还有一个隐性需求:这个链路涉及智驾、XData、framework 多方,出了问题需要不扯皮的证据,而不是各说各话。

方案 ​

14 跳链路 ​

mermaid
flowchart TD
    P["智驾感知 20Hz<br>SD / AP 双路 over-all 整包"] --> DP["driving_sensor_parser<br>FlatBuffers 解析"]
    DP --> SD["SignalDispatcher"]
    SD --> EB["ExternalBizService<br>工作线程池"]
    EB --> H["BarrierGateHandler<br>FOV 筛选 + 双源 min 合并<br>4-case FSM + 5Hz 限流"]
    H -->|"Float 130102"| JT["JniTransport<br>mSignalCodes 白名单"]
    JT --> DC["DataCenter<br>SharedFlow.tryEmit"]
    DC --> EV["BarrierGateEvent.collect"]
    EV --> HP["CarIntelligentHelper"]
    HP --> FW["framework<br>CarIntelligentEngineManager"]

SD/AP 两路都无条件解析(早已退订按模式路由的旧方案),各自从障碍物数组里按 Type == eBarrierGate(20012) 过滤,维护各自的最新闸机缓存 latest_sd_gate_ / latest_ap_gate_;每帧 mergeAndDecide 合并:双源都有取 min(dist),单源有用单源,双源都无才按状态机处理「有→无」。某路缓存超过 100ms 无新帧(约 2 帧)就不再参与合并,避免旧闸机永久存活。

状态机与限流 ​

mermaid
stateDiagram-v2
    [*] --> D: caseD 持续无
    D --> A: 无→有 即时发距离
    A --> C: caseC 持续有
    C --> C: 满 200ms 发最新 min dist
    C --> B: 有→无 即时发 -1.0 哨兵仅一次
    B --> D: 持续无不重复发送

关键不变量:5Hz 限流只作用于 caseC(持续有),caseA/caseB 翻转即时发送不被 200ms 卡住——业务响应延迟最坏一个 20Hz 周期,即 50ms 以内。哨兵 -1.0 在同一次「有→无」翻转里只发一次。坐标口径统一为 +Y 正前、+X 横向,FOV 角度用 atan2(x, y),距离取平面距离 sqrt(x²+y²);passable(栏杆开/关)只进日志,不参与选中逻辑。

三层可观测 ​

  • L1 统计层:每 30s wall-clock 窗口输出 4 行中文汇总(来帧频率、闸机识别、状态变化、解析与异常),第一行就有「结论=」字段直接给判定。所有「率」以 wall-clock 为分母,不以 frame count(20Hz 本身不稳定)。
  • L2 事件层:caseA/B 翻转即时打印、caseC 实际分发 5Hz 但日志降为 1Hz、异常首报即时 + 30s 限频。每条 FSM 行带 frame/decision/dispatch/timestamp 四元组。
  • L3 调试层:每帧每个闸机的 raw 字段,默认关闭,只能由运行时属性显式打开,无任何默认开启路径。

配套 11 类异常证据矩阵:从包络非法、FlatBuffer 校验失败、对象空指针、坐标 NaN/Inf、时间倒退到极远距离,每类都有专属计数 + 独立限频告警,排查时先看 L1 中文结论再按 tag grep 首报细节。FOV 和调试开关都支持 setprop 运行时调参,handler 每 1s 采样一次属性值,约 1s 生效;非法值(比如 FOV 设 181)直接拒绝并保持旧值,不会回退到「同一秒内曾经合法」的值。

AI 排障 Skill:四模式 ​

mermaid
flowchart LR
    RAW["原始日志<br>.alog / .xlog / .xp / 目录"] --> DEC["log-decoder<br>递归解密"]
    DEC --> EXT["Extract 链路提取<br>218MB → 412KB"]
    EXT --> DIA["Diagnostic<br>14 跳根因卡片报告"]
    EXT --> QRY["Query<br>任意切片问答"]
    EXT --> RPL["Replay<br>单文件 HTML 动画回放"]

Extract 把动辄几百 MB、上千万行的原始日志一次性抽出 14 跳所有相关行——实测 218MB / 1234 万行压到 412KB / 3124 行,压缩率 99.97%,后续所有 grep/awk 都基于这个小文件,查询提速约 500 倍。Replay 是一个零依赖、不连 CDN、离线可用的单文件 HTML 播放器:浏览器本地 File API 读日志,自车居中、120° FOV 扇形、闸机按位置/Id/距离/passable 渲染,时间轴可拖、0.25×~5× 变速,还有密度感知——对象行密度不足 50% 时顶部给警告,提醒去开联调日志开关。给 PM/QA 演示「那一刻车感知到了什么」,发一个 HTML 加一份日志就够。

关键实现 ​

FOV 筛选的坐标语义是最大暗坑。 上游 parser 对 proto 坐标做过轴交换,FlatBuffer 里 Pose 的 +Y 才是前方,所以角度必须从 +Y 轴起算:atan2(Pos.X, Pos.Y),0° 即正前方。2026-06-18 之前的版本误用了 atan2(Pose.Y, Pose.X)——轴参数写反,正前方的闸机角度被算成 90~180°,直接全部落在 FOV 外,画面里明明有闸机就是不下发。这类「数值有限但语义错误」的 bug 靠日志计数器看不出来,最后是靠逐帧举证日志里的 angle 字段人眼看出来的,这也是后来坚持做 [FSM_OUTFOV] 证据行的原因:量产默认不开 L3 也能看到 FOV 外对象的 Id/angle/dist。

日志 marker 必须写进 format 串。 早期版本把 [BG_OBJ] 当成日志库的 tag 参数,依赖框架自动前置;结果在不限频路径上框架早返回、根本不注入 tag——日志里没有 [BG_OBJ] 字面量,下游所有正则匹配为 0,回放和 Skill 全部瞎掉。修复方式是把 marker 显式塞进 format 串,限频和不限频两条路径都能被正则命中。教训:凡是给机器消费的日志,关键字必须在你自己控制的字符串里。

用错轴问题的实车乌龙。 2026-06-23 实车测试报「过闸机没数据」,我先怀疑轴又错了,出了调试包加 PARK 模式解析和举证日志,台架回放确认三个闸机(自驾、人驾)全部正确识别、inFov/距离都对——最后结论是那次测试全程泊车模式过闸机,而需求约定泊车模式本就不透出闸机数据,链路本身没坏。没有那套举证日志,这个「bug」不知道要查多久。

下游证据要标强度,不能装作全知。 Handler 打了分发日志只证明调了 dispatch();SignalDispatcher 的公共日志只证明观察者列表非空;JNI 转换日志位于 Java 回调之前;而 BarrierGateEvent 正常 collect 没有成功日志、helper 的 manager 为 null 会静默丢弃。Skill 的 Diagnostic 报告把这些证据边界逐条写死:能定位到哪、不能断言到哪,证据不足就标「⚠ 证据不足」,不允许编造。出现 No virtual method setBarrierGateDistance(F)V 时才能一锤定音——native 和 Java 全通,卡在 framework jar 没同步新 API。

指标 ​

指标数值
单帧处理耗时平均 20μs,P95 30μs,最大 80μs
翻转响应延迟≤50ms(一个 20Hz 周期)
出口频率caseC 实测 4.5~5.5Hz(5Hz 限流)
运行时调参生效setprop 后约 1s
链路日志提取218MB / 1234 万行 → 412KB / 3124 行(99.97%)
下游查询提速约 500×(基于提取产物)

复盘 ​

  1. 轻量需求的重量全在边界上:14 跳里最容易漏的是 JNI 白名单和 helper 寻址这两处跨边界点,所以 SOP 里专门把它们标红,自验证清单覆盖 4 个 case + FOV 边界 + 限流 + 开关关闭路径。
  2. 可观测要分层且诚实。L1 给结论、L2 给事件、L3 给逐帧,各管各的密度;每条日志能证明什么、不能证明什么,写进 Skill 的硬边界里,排查报告才不会过度承诺。
  3. 给机器消费的日志和给人看的日志是两种东西:前者 marker 必须可控、格式必须稳定,后者要中文、要给结论。混着设计,两头都受害。
  4. AI Skill 的瓶颈从来不在模型,在预处理。218MB 压到 412KB 之后,四模式(Diagnostic/Query/Extract/Replay)全都变得便宜,Replay 那个零依赖 HTML 播放器是投入产出比最高的一个——非工程同事终于能自己「看见」现场。
  5. 这套链路的传输机制(SignalDispatcher 分发、JniTransport 白名单)和 Native-Unity 信号链路 是同一套基础设施,可观测方法论则延续自监控流水线的实践。

罗子初 · AI 座舱专家 · 8年端侧研发

Lv.1