标签: cereal

  • WebRTC 数据桥:bridge_services_in 是请求不是授权

    结论:WebRTC cereal 数据桥的核心边界是——bridge_services_in 只能是「请求」,不是授权结果;/schema 只是结构提示,不是权限清单。出站代理按请求里的主题列表订阅 SubMaster,仅对 updated 的主题发送 {type, logMonoTime, valid, data} JSON;入站代理只取 JSON 的 type/data,用本地 monotonic() 生成新时间、valid 默认 False。因此 sendcan、carControl 与固件配置主题都不应作为通用桥写入目标。

    一、出站:消息内容、时间与编码

    入口 源码行为 边界
    CerealOutgoingMessageProxy.__init__()、update() 按请求主题列表建立 SubMaster;每轮 sm.update(0),仅对 updated 的主题发送 JSON 到所有开放通道。 sm.update(0) 在 asyncio 任务中同步执行;多个服务同轮发布时逐一编码/发送,没有本地有界队列或每主题独立带宽限额。
    to_json()、run() struct 用 to_dict(),list 元素递归处理,顶层 bytes 用默认 UTF-8 decode();异常在任务外层记录后下一轮继续。 任意二进制 Data 不保证是 UTF-8;解码异常会使当前轮 update() 中断。

    出站 logMonoTime 与 valid 来自本地 SubMaster,属于原 cereal 事件的元信息;这两个字段会作为 JSON 呈现给对端,但它们不是入站代理会原样恢复的字段。仅用 updated 判断发送,也不代表服务消息通过了有效性或新鲜度门。

    二、入站:允许列表与重新发布

    入口 源码行为 边界
    StreamSession.message_handler()、run() 内建控制命令单独处理;普通 type 仅在会话的 bridge_services_in 列表里才转给入站代理,并在共享 DynamicPubMaster 创建发布 socket。 允许列表来自本地 /stream 请求体;本机服务没有按主题风险等级的固定全局许可表。
    CerealIncomingMessageProxy.send()、messaging.new_message() JSON 的 type、data 决定新事件类型与负载;非 dict 的 data 用 len(data) 作为 list/Data 大小;新事件 logMonoTime 取本机 monotonic(),valid 默认 False。 入站 JSON 里的 logMonoTime、valid 不被复制;调用点无消息体字节上限、数值范围白名单或单主题频率限制。
    DynamicPubMaster.add_services_if_needed() asyncio.Lock 保护动态加 socket,同一进程的 shared_pub_master 跨会话复用已注册主题。 锁保护的是同进程 socket 注册,不是来源认证或 msgq 主题授权;旧会话注册的 socket 仍可能留在共享实例。

    一个反直觉但重要的边界:valid=False 不等于所有消费端都会丢弃消息。源文档举例 joystickd_thread() 对 testJoystick 只检查接收帧年龄是否超过 0.2 秒,然后取 axes 形成 carControl,该路径没有检查 sm.valid['testJoystick']。这不是「普通远端请求能控制实车」的证明——还受进程模式、selfdriveState、车型参数、card 与 panda 安全门约束——但足以说明 valid 不能替代主题权限判定。

    入站侧还有两处与「成功」语义有关的边界。其一,动态发布器探针确认:同一次和跨次重复主题只建一个 socket,注册序列中后项失败时前项仍留在 socket 里、没有回滚,未知名称会直接下传给 messaging.pub_sock(),实际服务名能否注册由底层决定。其二,handle_post_notify() 向当前会话逐个发送原 JSON,某通道异常被跳过;没有会话或部分发送失败时仍返回 HTTP 200 OK。二者都说明「HTTP 成功」不等于「通知已被远端接收」,也不证明任意名称能成为真实 msgq 主题。

    三、/schema 是结构提示,不是权限清单

    handle_get_schema() 按逗号解析请求服务名,只检查它们是 log.Event 字段且不以 DEPRECATED 结尾,再调用 generate_field()。其中 generate_type() 把 struct 展开为字段映射,list 表示为单元素模板,enum 只写 "text",基础类型写 Cap’n Proto 类型名;generate_struct() 过滤废弃字段。它不包含字段编号、单位、频率、枚举成员、最大长度、schema 版本或读写权限。非法服务触发 assert,由 HTTP handler 的通用异常路径返回 500,而不是专门的 4xx 校验错误。

    源文档的五项原 schema 函数/假类型树探针确认:基础类型保留类型名,枚举压成 text,列表压成单元素模板;struct 和 group 都跳过名为 deprecated 或以 DEPRECATED 结尾的字段。这些输入是合成 Cap’n Proto 类型树,不能证明所有真实 schema 分支都能导出,或其结果能作为授权清单。

    四、硬件与插件重构验收

    普通诊断插件只应授予明确的只读主题 + 字段;写主题必须由宿主按调用方身份、会话、车型状态和安全等级批准。桥接协议应保留原始事件的采样时戳与传输时戳之区别,显式定义 valid 的重建策略、二进制编码方式、schema 版本、最大消息长度、速率和背压;下游不能以 valid=False 自动假设无执行副作用。

    回归覆盖应包含非 UTF-8 Data、嵌套字节字段、未知字段/服务、错误 list 长度、极大负载、重复主题注册、桥接重连,以及 testJoystick 无效位但新鲜消息的隔离行为。现有 WebRTC 会话测试只用 ASCII Data、一组结构/list 输入和单次代理发送,没有 schema 版本、任意二进制、外层有效位/时间戳保留或风险主题授权回归;本篇未运行上游测试,所有写入仍须经过车端独立安全链。

    常见问题

    valid=false 是不是等于消息一定被丢弃?

    不等于。入站代理生成的新事件 valid 默认 False,但消费端未必检查它:joystickd_thread() 对 testJoystick 只检查接收帧年龄是否超过 0.2 秒,不检查有效位。所以不能用 valid 替代主题权限判定,也不应假定无执行副作用。

    bridge_services_in 是授权结果吗?

    不是。它来自本地 /stream 请求体,只是「请求」,本机服务没有按主题风险等级的固定全局许可表。写主题必须由宿主按调用方身份、会话、车型状态和安全等级批准,sendcan、carControl 和固件配置主题不作为通用桥写入目标。

    /schema 能当权限清单用吗?

    不能。它只检查服务名是 log.Event 字段且不以 DEPRECATED 结尾,输出 struct 字段映射、list 单元素模板和 enum 的 "text",不含字段编号、单位、频率、枚举成员、最大长度、schema 版本或读写权限;非法服务还会触发 assert 返回 500。

    桥接协议该保留哪些时间字段?

    应显式区分原始事件的采样时戳与传输时戳。出站的 logMonoTime 来自本地 SubMaster,入站却改用本机 monotonic() 且不复制传入值;若不加区分,下游无法判断数据新鲜度,也无法重建 valid 语义。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/WEBRTC_CEREAL_SCHEMA_BRIDGE_CONTRACT_DEEP_DIVE.md(取用了出站/入站桥接语义、/schema 表达能力边界与插件验收要求)。

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

    更新记录

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

  • cereal 消息契约:schema、服务表、valid 与落盘各管什么

    结论:cereal 的消息契约必须拆成四层看:schema 定义、services.py 服务表配置、运行消息有效性、loggerd 落盘结果。union 分支存在不表示进程正在发布,也不表示 loggerd 会记录;logMonoTime 是消息封装时间,valid 也只表示运行有效位,不承诺车辆总线帧通过车型解析或 panda 安全检查。把「字段存在」当作「数据已发布或已记录」是这份契约最常见的误读。

    四层契约各管一段

    schema 层,Event 有 logMonoTime、valid 与 union,carState、carControl、carParams 指向 opendbc 的 car.capnp,customReserved* 保留固定 field ID。服务表层,SERVICE_LIST 声明服务名、期望频率、should_log、qlog decimation 和队列容量,build_header() 生成对应的 C++ 表——频率是配置与健康检查依据,不等于真实发布速率。

    消息构建层,new_message() 默认 valid=False,把 logMonoTime 设为单调时钟纳秒并初始化主题,发布者通常再赋有效位。日志订阅层,loggerd_thread() 订阅 should_log 服务、非直播 EncodeData 和可选原始音频;普通消息按配置写 rlog,按计数抽样写 qlog。因此收到消息、valid=True、内容可信和时戳新鲜是四个不同判断。

    层 源码与含义 不能推出的结论
    schema Event 有 logMonoTime/valid/union;车型消息指向 car.capnp union 分支存在不表示进程正在发布或 loggerd 会记录
    服务表 SERVICE_LIST 声明频率、should_log、decimation、队列容量 频率是配置依据,不等于真实发布速率;should_log=False 也可能有专门编码路径
    消息构建 new_message() 默认 valid=False,设置单调时间 收到、有效、可信、时戳新鲜是不同判断
    日志订阅 loggerd_thread() 按 should_log 与计数抽样写盘 qlog 不是每个主题的完整回放数据

    关键主题与字段语义

    CAN 接入侧,Event 的 can 是从车接收帧,sendcan 是主机候选发送帧;CarState.canValid/canTimeout/canErrorCounter 表示车型解析健康,不能只取上次解码值。services.py 为 can 设大队列与 qlog 高抽样率,因此完整 CAN 分析应使用 rlog。执行决策侧,enabled、latActive、longActive 不等价,Actuators 的扭矩/转角/曲率属互斥横向命令语义,纵向 accel 为 m/s²。

    模型到规划侧,modelV2 的 qlog 抽样未配置,不应拿 qlog 复原所有推理帧;LateralManeuverPlan 是调试覆盖曲率,不是一般模型输出。使能与监督侧,enabled 与 active 分开,警报文本/声音、人机监督策略与注意力状态在独立消息中。摄像头与编码侧,帧标识、曝光时间戳、编码索引/时间戳和视频字节是不同层,图像主数据经 VisionIPC,EncodeData 由 loggerd 分离写媒体文件。

    链路 载荷定义 核心字段与边界
    CAN 接入 Event.can/sendcan、CarState canValid/canTimeout/canErrorCounter 表示解析健康
    执行决策 CarControl、CarParams enabled/latActive/longActive 不等价;safetyConfigs/dashcamOnly/passive 决定执行边界
    模型到规划 ModelDataV2、LongitudinalPlan、LateralManeuverPlan 模型含帧 ID、EOF 时间、耗时、轨迹;modelV2 qlog 抽样未配置
    使能与监督 SelfdriveState、DriverMonitoringState enabled 与 active 分开;监督策略独立消息
    panda 安全状态 PandaState controlsAllowed、模式/参数、RX 无效、TX 阻断、心跳丢失分列
    摄像头与编码 FrameData、EncodeIndex、EncodeData 帧标识、时间戳与视频字节分层;EncodeData 分离写媒体文件

    需要单独记住:Event 的 logMonoTime 是消息封装时间;相机 timestampEof、模型 timestampEof、GNSS Unix 时间和 IMU 自有时间字段并不自动相等。传感器/定位插件必须显式声明时钟域、单位、采样时点和迟到处理。

    Route 与 Segment 文件如何落盘

    元数据由 logger_build_init_data() 记录版本、设备、内核、Git 信息、参数和部分硬件日志,参数值只有不带 DONT_LOG 标志才写入。命名/开段阶段,路由名由递增计数与随机后缀组成,段目录为 <route>--<part>;开段创建 rlog.lock、rlog.zst、qlog.zst,写 initData 与 START sentinel,结束段/路由写 END sentinel 并移除锁。

    压缩阶段,rlog.zst、qlog.zst 是 zstd 压缩的连续 Cap’n Proto Event 字节流,qlog 只在抽样命中时附写。切段时,rotate_if_needed() 与 handle_encoder_msg() 等已出现的编码器进入新段后切段,相机/编码器异常时按时间回退切段;新段编码数据须等关键帧,先于关键帧的帧被丢弃。媒体文件按相机表写 fcamera.hevc、ecamera.hevc、可选 dcamera.hevc 与 qcamera.ts;RecordAudio 打开时 rawAudioData 还会写入 rlog(decimation=None,不进 qlog),这涉及额外的数据保留与隐私审查。

    抽样率是理解落盘差异的关键:can 的 qlog 抽样率为 2053、sendcan 为 139、carState 为 10、modelV2 为 None。编码索引虽通常 should_log=False,但在 write_encode_data() 中专门转写且始终进 qlog。不要用 qlog 作为控制/安全故障分析的唯一依据。

    对插件化与硬件的最低要求

    1. 插件 schema 需要版本、字段号与兼容策略;Cap’n Proto union 的固定编号不能任意复用,customReserved* 约束也不能未经全链路兼容评估就改控制主契约。
    2. 发布权限应按主题分级:观测只读、规划候选、carControl 和 sendcan 分开授权;valid、新鲜度、总线校验与硬件许可各自独立。
    3. 存储插件需保留元数据、段边界、单调时钟、媒体索引、丢帧及部分文件状态;为取证保留 rlog,不可用 qlog 抽样代替。

    常见问题

    消息字段存在,是否说明该数据一定被记录?

    不代表。四层契约是分开的:schema 里 union 分支存在,不表示受管进程正在发布,也不表示 loggerd 会记录;服务表里的频率是配置与健康检查依据,不等于真实发布速率;只有 loggerd_thread() 按 should_log 与抽样配置实际写盘的内容才在日志里。

    为什么不能用 qlog 复原所有推理帧?

    因为 modelV2 的 qlog 抽样配置为 None,carState 抽样率为 10,can 为 2053,各主题抽样并不一致,qlog 只在抽样命中时附写。源文档明确 qlog 不是每个主题的完整回放数据,控制/安全故障分析不要以它作为唯一依据。

    Event 的 logMonoTime 就是传感器采样时间吗?

    不是。logMonoTime 是消息封装时间;相机 timestampEof、模型 timestampEof、GNSS Unix 时间和 IMU 自有时间字段并不自动相等。传感器/定位插件必须显式声明时钟域、单位、采样时点和迟到处理。

    valid=True 是否表示这一帧数据可信?

    不等于。valid 只是运行有效位,源文档明确它「不承诺车辆总线帧通过车型解析或 panda 安全检查」。车端数据是否可信还要看 CarState 的解析健康字段、时间戳新鲜度以及 PandaState 中的许可与故障分列。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/MESSAGE_LOG_CONTRACTS_DEEP_DIVE.md(取用了四层契约、关键主题字段语义、Route/Segment 落盘流程与 qlog 抽样率)。
    • 项目 02 —— openpilot-research/LOGGERD_SERVICE_SAMPLING_POLL_CONTRACT_DEEP_DIVE.md(仅用于确认 loggerd 逐服务订阅与计数抽样相位的边界)。

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

    更新记录

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