结论: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 语义。
延伸阅读
- WebRTC 会话与 HTTP 生命周期边界 —— 承接会话替换、busy 与端口的整体边界。
- 远程 RPC 与 WebRTC 的边界 —— 说明本机认证与授权为何必须分离。
- WebRTC 码率与关键帧时序 —— 补充出站无每主题带宽限额的后果。
- 界面告警、驻车更新与远程服务:不在控制闭环内的进程边界 —— 同一批服务边界的整体视图。
- UDS 诊断工具为什么不能随手调:只读、会话与写 ECU 的分权设计 —— 对照只读与写入权限的分级思路。
参考资料
- 项目 02 ——
openpilot-research/WEBRTC_CEREAL_SCHEMA_BRIDGE_CONTRACT_DEEP_DIVE.md(取用了出站/入站桥接语义、/schema表达能力边界与插件验收要求)。
说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。
更新记录
首次发布 2026-10-11 最近核对:2026-10-11
发表回复