结论:在这套 CAN 与 Cap’n Proto 适配层里,valid 只在 Event 外层,帧本身没有独立有效位:编码器只写外层 logMonoTime 与 valid,解码器只返回时间戳与帧三元组,不返回外层有效位。更关键的是发送侧——C++ 发送线程只读 logMonoTime 和 sendcan,用一秒年龄门与 fake_send 条件决定是否调用打包发送,没有检查 event.getValid(),因此 CS.canValid=False 只是日志与下游可见的标注,并不单独阻止该线程尝试发送。是否实际发上车总线仍受后续编码、传输与 MCU 安全钩子控制。本文为固定源码快照 c8fb906 的静态阅读与假 Event 原函数探针,未运行真实 Cap’n Proto、总线进程或连接车辆。
组件与关键函数
CAN 帧在 schema 里有 32 位地址、数据载荷和 8 位来源;Event 有 logMonoTime、默认恒真的 valid,且 can 与 sendcan 是不同 union 成员。编码器首次遇到非空帧时缓存字段对象,后续按相同 schema 访问。
| 函数/组件 | 行为 |
|---|---|
CanData/Event |
帧有 32 位地址、数据载荷与 8 位来源;Event 有 logMonoTime 与默认真的 valid |
_get_reader_fields()/_get_writer_fields() |
首次非空帧时缓存字段对象,后续按相同 CanData schema 访问 |
can_list_to_can_capnp() |
新建 Event,填入 can 或 sendcan 帧列表并序列化;valid 与时间戳只写在外层 |
can_capnp_to_list() |
逐条解析 bytes,返回时间戳与帧三元组,不返回 Event valid |
controls_update() |
控制侧用 CS.canValid 标注 sendcan Event |
can_send_thread()/can_recv() |
发送侧按时间戳与一秒年龄门筛选;接收侧把接收布尔值设为 Event valid |
从帧到 Event 的数据流
编码器接受三元组列表,按调用方 msgtype 在 Event 上初始化 union 列表,外层 logMonoTime 来自单调时钟。首次遇到非空列表才缓存 CanData 的三个 schema 字段对象;空列表仍可产生合法的空 can 或 sendcan Event。函数自身不检查地址范围、总线号、CAN 或 CAN FD 合法长度与消息数量,依赖 schema 字段类型以及后续边界处理——因此不能把 schema 能序列化当作总线发送合法性证明。
控制更新时,若控制消息存活则生成 sendcan,以状态里的 canValid 填外层 valid;而车型识别回调使用默认 valid=True。发送线程只读 logMonoTime 和 sendcan,用一秒年龄门与 fake_send 条件决定是否调用打包发送,没有检查 event.getValid()。所以状态标注与是否尝试发送是两件事,日志里看到的无效标注不能推断「危险动作已经发生」或「一定不会发送」。
接收侧与解码资源边界
接收侧即使底层接收返回假,仍构造一个有效位为假的 can Event,并放入当前收集到的帧。Python 解码只保留时间戳与帧、不传递外层有效位;状态更新先把这些帧送给车型状态与雷达接口,再以原始消息列表是否非空判断本轮是否收到 CAN。这样「收到一个无效 Event」仍算消息到达,其中已有的帧仍可能进入车型解析——当然车型解析器本身还有 CAN 检查,这里的结论仅限适配层与状态更新的直接门控。
解码函数用接近无穷的遍历上限解析输入,相当于不在此调用处施加实用的遍历预算,输入消息大小和队列限制需沿上游单独核对。它直接按 msgtype 取 union 字段,没有先核对 union 实际类型,调用方传入错误类型或坏 bytes 时可能抛异常,而非返回结构化的坏消息结果。字段对象全局缓存假设后续仍是同一个 CanData schema,当前正常路径成立;缓存不保存消息内容或设备状态,也不等于逐帧验证。
离线原函数探针与证据范围
四项隔离测试从归档文件抽取四个原函数运行,仅替换 Event、帧字段对象与序列化存储。
| 输入 | 原函数观察结果 | 证据边界 |
|---|---|---|
空 can 列表 |
可生成并解出空 Event;字段缓存仍为空 | 假 Event 只实现最小接口,不证明真实序列化字节 |
非空 sendcan,有效位为假 |
帧与时间戳往返;外层有效位不出现在解码结果;首次帧填充缓存 | 只证明本适配函数的字段路径与返回形状 |
| 错误 union 与坏载荷 | 假 Event 抛出的属性或解析异常直接向上传播 | 真实解析库的错误类型与 union 语义仍未验证 |
| 缺字段三元组、非法编码类型 | 原函数没有预检查;前者索引失败,后者委托初始化失败 | 假 Event 只模拟非法 union 拒绝,不能外推 schema 约束 |
回放与仿真也复用这套适配函数:回放会把日志中的 can Event 转 bytes 后经该解码器导出帧列表,仿真则复用编码器发布合成 can。它们既服务实车运行也服务回放,但信任级别与时间域并不相同。
自研硬件与插件契约
消息插件契约应明确区分载荷有效位、总线链路健康、schema 可解析、时间戳年龄和 MCU 安全放行。只读观察插件可拿到带来源与有效位的 CAN 快照;控制候选插件不得直接发布 sendcan 到受信任网关。网关需逐 Event 核对 union 类型、有效位、时间域与年龄、帧数、地址、总线、长度和车型授权,再把送达传输、MCU 接受与安全拒绝分开记录。诊断与回放输入应有明确大小与遍历预算,异常帧应隔离而非让主车控线程意外退出。离线验收至少覆盖空列表、错误 union、有效位为假但含帧、CAN 事件无效且仍有先前好帧、坏 bytes、极大列表与载荷、未来或跨时钟域时间戳。
常见问题
CS.canValid=False 会阻止发送吗?
不会单独阻止。控制更新会把 CS.canValid 写进 sendcan Event 的外层有效位,但 C++ 发送线程只读 logMonoTime 与 sendcan,用一秒年龄门和 fake_send 条件筛帧,没有检查 event.getValid()。是否真正发上车仍受后续编码、传输与 MCU 安全钩子控制。
为什么解码后拿不到 valid?
因为有效位只在 Event 外层。编码器把 valid 与时间戳写在外层,解码函数只返回时间戳与帧三元组列表,不返回外层有效位;因此跨语言往返后,帧层面看不到原始的无效标注,需要在上层另行传递。
收到一个无效 CAN 事件算收到消息吗?
在这层算到达。接收侧即使底层接收返回假仍会构造有效位为假的 can Event 并放入已收集帧;状态更新以原始消息列表是否非空判断本轮是否收到 CAN,所以无效事件里的帧仍可能进入车型解析,导致「丢帧」判断被低估。
探针证明了真实 Cap’n Proto 行为吗?
没有。四项探针替换的是 Event、帧字段对象与序列化存储,属于局部回调与编码器证据;真实解析库的错误类型、union 访问语义、遍历预算下的内存与时间行为都未验证,也不是诊断帧取得板级发送授权的证明。
延伸阅读
- CAN 报文解析的「健康位」在说什么:校验、计数器与总线空窗 —— 承接本文有效位在解析层的最终归属。
- 车辆消息字段契约:从 DBC 到状态量的边界 —— 解释帧三元组之后字段语义如何被消费。
- panda 健康 ABI 与主机版本映射 —— 对照 MCU 安全钩子与主机侧有效位的分工。
- 从使能到车轮:执行链上的八道门,以及它们各自不保证什么 —— 说明发送尝试之后仍有独立安全门。
- ECU 诊断传输层契约 —— 对照诊断帧与本文控制帧在传输层的差异。
参考资料
- 项目 02 ——
openpilot-research/PANDAD_PYTHON_CAN_CAPNP_ADAPTER_VALIDITY_DEEP_DIVE.md(取用了编码与解码函数、发送侧不读有效位、接收侧无效事件构造与四项原函数探针)。 - 项目 02 ——
openpilot-research/CAN_PARSER_HEALTH_DEEP_DIVE.md(仅用于确认车型解析器自身的 CAN 检查位置)。
说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。
更新记录
首次发布 2026-10-11 最近核对:2026-10-11
发表回复