标签: 消息契约

  • 车辆消息字段契约:请求、软件回报与硬件执行的三层之分

    结论:车端消息契约里最容易混淆的是「请求、软件回报、硬件执行」三层。CarOutput.actuatorsOutput 只是软件计算后的回报,不表示 panda TX 钩子接受、ECU 已执行或车辆真实响应;CarState.valid 直接取 CS.canValid,不自动合并全部逐字段新鲜度;SafetyConfig 虽是列表,但当前所读 setSafetyMode() 只取 safetyConfigs[0]。schema 注释是意图,运行时许可必须以代码及 panda 固件为准。本文为固定源码快照 c8fb906 的静态阅读,未运行回放或测试。

    生产和消费链

    card.state_update() 从 CAN 批次构造 CarState,card.state_publish() 发 carState、carOutput 和周期性 carParams;controlsd 用 CarState、selfdriveState 与计划形成 CarControl,再由车型控制器转成候选 CAN 帧。CarParams 还在启动时写入 Params,pandad 需等待 FirmwareQueryDone 和 ControlsReady 后读取其安全配置。

    消息 生产者 主要消费者
    CarState card.state_update() 状态机、控制器
    CarControl controlsd 车型控制器 apply()
    CarOutput card(取上次保存值) 控制器饱和逻辑参考
    CarParams 启动时写入 Params pandad 读取安全配置

    把这条链当成单向管道同样不准确。同一轮里 controlsd 既消费 CarState,又把自己的输出回报成 CarOutput 供下一轮饱和判断使用;CarParams 虽在启动时写入,却是 pandad、控制器与规划层共同读取的配置来源。理解这种「请求—回报—配置」的多向关系,是判断某个字段能否作为执行依据的前提。

    CarState:归一化车况

    字段簇 schema 语义 运行时注意点
    CAN 健康 canValid、canTimeout、canErrorCounter 前两者由车型接口汇总解析器健康;canErrorCounter 累计的是空 CAN 接收周期,不是校验和错误计数;顶层 carState.valid 直接取 CS.canValid
    速度与巡航 vEgo 估计、vEgoRaw 原始轮速、vEgoCluster 仪表速度 单位/估计来源有别,不能把仪表显示值直接反馈给动力学控制;CruiseState 区分可用、已启用和静止
    驾驶员与转向 踏板为用户输入;驾驶员扭矩和 EPS 扭矩使用原生 CAN 单位,另有 steeringPressed、steeringDisengage 与暂时/永久故障 跨车型不得把原生扭矩直接比较;原车 AEB/LKAS/FCW 故障与功能状态是不同字段
    交互与车身 档位、按键边沿、转向灯、门、安全带、盲区 ButtonEvent 应在变化时发送;布尔默认假值不代表传感器已观测到安全无障碍

    车型基类先更新所有 CANParser,再调用品牌 CarState.update(),最后计算 canValid/canTimeout、仪表速度迟滞及 buttonEnable。需要明确:canValid 并不是所有源的数字签名。

    逐字段看,vEgo、vEgoRaw、vEgoCluster 三者单位相同但含义不同:一个是估计值,一个是原始轮速,一个是经迟滞处理的仪表速度。把它们当作可互换的「车速」会导致控制层用到被截零或迟滞过的值。同理,驾驶员扭矩与 EPS 扭矩使用原生 CAN 单位,跨车型直接比较会得到错误结论。

    CarControl 与 CarOutput:请求和回报

    CarControl.enabled 是总使能,latActive/longActive 分别表达横纵向活动。controlsd.state_control() 从 selfdriveState、车况故障、低速门、纵向覆写事件及 CarParams.openpilotLongitudinalControl 计算它们,三者不应合成一个开关。Actuators 中 torque、steeringAngleDeg、curvature 是按控制类型选用的横向请求,accel 是纵向 m/s² 请求;另有 gas、brake、torqueOutputCan、speed 用于记录车型控制器实际计算的下层值。

    字段 含义 不能推断的结论
    CruiseControl 取消/恢复/覆写意图 不能等同具体 CAN 帧,需品牌控制器映射
    HUDControl 发给原车仪表的提示数据 同样是请求,不代表仪表已响应
    CarOutput.actuatorsOutput 车型控制器计算值,注释称反映发送前的车型限幅/特性 不是 panda TX hook 接受、ECU 已执行或车辆真实响应的确认
    carOutput.valid 取 carControl 的 all_checks 不能升级成物理回执

    controlsd 会比较 CarOutput 与本轮 CarControl.actuators,估计转向受限以影响控制器饱和逻辑。controlsd 发布 carControl 时把外层 Event.valid 设为 CS.canValid;card.controls_update() 的本地门控是 carControl 存活后调用品牌 apply(),并用 CS.canValid 标记 sendcan 的外层有效位。它不在这一函数再次以 carControl.valid 为硬许可条件;完整许可仍依赖上游状态机、车型控制器及 panda 安全层。

    这里存在一个容易忽略的非对称:carControl 的外层 Event.valid 取 CS.canValid,而 card.controls_update() 在调用品牌 apply() 时并不再以 carControl.valid 为硬条件,只要求 carControl 存活。也就是说,消息层有效性、上游状态机、车型控制器与 panda 安全层各自负责一段,任何单一有效位都不能代表整条链已获授权。

    CarParams:跨进程配置

    字段簇 作用及信任界限
    身份/证据 brand、carFingerprint、VIN、carFw、fingerprintSource、fuzzyFingerprint 记录识别结果;来源标记不是身份认证或执行许可
    动力学/调参 质量、轴距、重心、转向比、惯量、轮胎刚度、PID/扭矩调参、执行器延迟进入规划控制;标称值需按目标车辆验证
    执行配置 pcmCruise、最低速度、横纵向控制能力、steerControlType、safetyConfigs、alternativeExperience、passive、dashcamOnly 决定不同层行为;任意单字段都不是最终执行许可
    认证状态 secOcRequired、secOcKeyAvailable 记录车型需求和密钥可用状态;不能把「存在密钥」理解为报文已通过对端认证

    SafetyConfig 是 (safetyModel, safetyParam) 列表,但当前所读 PandaSafety::setSafetyMode() 对其连接的 panda 只取 safetyConfigs[0] 并设置 alternativeExperience;多 panda 的分配须看上层设备编排。card 在被动模式会把列表替换成 noOutput。

    身份/证据类字段尤其需要小心:fingerprintSource 与 fuzzyFingerprint 只是来源标记,说明识别结果是如何得到的,而不是身份认证;secOcKeyAvailable 说明密钥可用,也不表示对端报文已通过认证。两者都不能升级为执行许可。动力学与调参字段同理,它们是标称值,进入规划控制前必须按目标车辆验证。

    RadarData 与扩展边界

    RadarData.errors 包含 CAN 错误、雷达故障、错误配置及临时不可用;点数据包含轨迹 ID、相对距离/横移/速度。schema 注释称这些点字段必需,但 Cap’n Proto 默认值仍可能存在,消费者应结合生产者健康和点轨迹年龄;card 只在收到 RD 时发 radarTracks,外层 valid 由错误位汇总。对自制硬件和插件化,建议将字段单位、坐标系、采样频率、时间戳、最大年龄、缺失值与信号来源做成版本化契约,并把外层 valid、字段默认值、软件输出回报和硬件执行确认分别建模。

    常见问题

    CarOutput 能证明车辆真实执行了吗?

    不能。它来自车型控制器 apply() 的计算值,注释称反映发送前的车型限幅/特性,是软件计算后的回报;不是 panda TX 钩子接受、ECU 已执行或车辆真实响应的确认。

    carState.valid 为真代表所有字段都新鲜吗?

    不代表。顶层 carState.valid 直接取 CS.canValid,并不自动合并全部逐字段新鲜度;消费关键字段时仍需结合解析器健康与字段年龄判断。

    SafetyConfig 列表里的每个配置都会生效吗?

    不能直接推断。当前所读 setSafetyMode() 只取 safetyConfigs[0] 并设置 alternativeExperience;多 panda 的分配须看上层设备编排。

    CarControl 里的计划字段等于具体 CAN 帧吗?

    不等于。CruiseControl、HUDControl 等是意图与提示数据,需要品牌控制器映射为具体帧;schema 字段不能直接等同 CAN 报文。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CAR_MESSAGES_FIELD_CONTRACT_DEEP_DIVE.md(取用了生产消费链、CarState/CarControl/CarOutput/CarParams 字段簇、RadarData 与扩展边界)。
    • 项目 02 —— openpilot-research/ECU_DIAGNOSTIC_TRANSPORT_DEEP_DIVE.md(仅用于确认指纹与固件证据进入 CarParams 的位置)。

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

    更新记录

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

  • openpilot 系统底座:manager、cereal 与 msgq 分工

    结论:openpilot 的系统底座由四类互不等价的服务组成:manager 是进程生命周期的控制平面,cereal/msgq 是实时结构化消息数据平面,VisionIPC 单独传图像缓冲,Params 保存跨重启配置。不能把它们合并理解为一个「事件总线」,因为四者对可靠性、持久性和时序的承诺完全不同:msgq 是可被新写入覆盖的实时队列,而 Params 才承担持久化。本文为固定源码快照 c8fb906 的静态阅读,未在真实设备上启动完整系统、未验证断电与共享内存溢出的实际时序。

    控制平面与数据平面:四类服务的分工

    把 manager、cereal、VisionIPC 和 Params 当成同一类基础设施,会导致对可靠性和时序的错误假设。它们的职责边界在源码里是清晰分离的:manager 负责进程启停,cereal/msgq 负责实时消息,VisionIPC 负责图像缓冲的文件描述符传递,Params 负责跨重启的键值配置。

    层 代表入口 语义边界
    manager 控制平面 manager_init()、manager_thread()、ensure_running() 订阅 deviceState、carParams、pandaStates,按 started 与点火边沿管理进程;状态消息是报告,不是安全固件许可
    cereal/msgq 数据平面 new_message()、PubMaster.send()、SubMaster.update() 实时结构化消息;新插件若依赖每帧必达,需自建可靠传输,不能把该队列当事务消息系统
    VisionIPC VisionIpcServer::listener()、VisionIpcClient::recv() 只传图像缓冲索引与元数据,图像内容不复制进普通 Event
    Params Params::put()、Params::get()、Params::clearAll() 持久配置与少量控制状态,按生命周期标志清理;不适合替换高频 cereal 消息

    启动与进程管理

    manager_init() 清理按生命周期标志定义的参数,填默认值和版本/设备信息,建立共享内存路径并注册设备;注册失败会抛异常,因此不能假定 manager_thread() 已经开始。manager_thread() 订阅 deviceState、carParams、pandaStates,按 started 与点火边沿清理参数、更新 IsOffroad、调用 ensure_running() 并发布 managerState。NOBOARD、BLOCK 等配置会排除对应进程。

    入口 已核对行为 关键边界
    ensure_running() 用 enabled、排除列表及 should_run(started, params, CP) 判定每个进程的目标状态,先停后启 条件按每次循环重算,不能假定 44 个进程同时运行
    ManagerProcess.stop() 先发 SIGINT 或指定信号,等待后必要时 SIGKILL block=False 只发起停止,下次启动会等待停止完成
    PythonProcess.start() / NativeProcess.start() 分别用多进程包装 Python 模块与原生可执行文件 插件进程若纳入管理,需保留进程状态与退出语义
    DaemonProcess.start() 用持久 PID 与 /proc 命令行子串判断是否复用守护进程 不设置普通 proc 对象,stop() 为空

    进程的入口和运行条件由 process_config.py 的静态表决定,因此插件化不能只实现动态 import,还需定义能力声明、依赖、健康状态及受管退出协议。收到卸载、关机、重启参数会退出循环,manager_cleanup() 清理受管进程后由 main() 调硬件关机或重启。此外,manager_thread() 仅在 deviceState 的存活、平均频率和有效位检查通过时更新电源监控看门狗文件,但进程启停仍读取缓存的 started,不受该健康门直接阻断。

    消息契约与底层队列

    log.capnp 与 services.py 分别定义 Event 的结构字段与主题清单、标称频率、日志选项和队列容量,改 schema 与服务表要同步消费者。new_message() 新建消息默认 valid=False 并写单调时钟 logMonoTime,生产者需要显式设置有效性;SubMaster.update() 轮询指定主题,更新帧号、消息时间与有效位;SubMaster.all_checks() 合并存活、平均频率和消息有效位,模拟模式会放宽部分时序检查。频率追踪以长期和近期平均频率判断接收速率,不是硬实时截止时间证明。

    底层 msgq_new_queue() 在 /dev/shm(macOS 为 /tmp)建立 mmap 队列,msgq_msg_send() 检查发布者 UID 后向环形缓冲写入长度与负载并唤醒读者,msgq_msg_recv() 在读者被逐出或读指针失效时重置到当前写指针,旧消息不保证可补读。SubMaster 的 conflate=True 订阅意味着主要消费最新状态。因此 services.py 里的频率只是契约/健康检查参考,并不表示所有消息在该频率上严格同步。图像则单独走 VisionIPC:VisionIpcServer::send() 只向 msgq 发送缓冲索引、服务器 ID 和帧附加元数据,VisionIpcClient::recv() 用服务器 ID 与索引确认缓冲仍属于当前服务端。

    Params 与生命周期配置

    Params 是 C++ 实现的 Python 绑定。底层 Params::put() 先写临时文件、fsync、加锁、原子 rename 并同步目录;Params::get() 可阻塞等待非空值;Params::clearAll() 依键标志清除指定生命周期数据,并清理未登记键。它适合持久配置和少量控制状态,不适合替换高频 cereal 消息。插件应声明键名、类型、默认值、清理标志与迁移版本,避免私自写未登记键后被 clearAll() 删除。

    设备启动门、记录与空间回收

    hardware_thread() 订阅 panda、外设、定位与辅助驾驶状态,生成 deviceState;点火/连接状态形成行车条件,热状态、启动训练、条款、空间、系统启动完成、更新状态及设备注册等构成启动门。进入行车后仍持续检查点火与严重过热,panda 超时会清除点火条件。需要强调:deviceState.started 是 manager 启停 onroad 进程的信号,不是 selfdriveState.enabled 或 panda 的 controls_allowed。硬件移植必须实现 HardwareBase 的平台能力并处理相机、IMU、温度、供电及看门狗;当前硬件选择仅按 /AGNOS 在设备与 PC 两个实现间切换。

    组件 关键函数 边界
    loggerd loggerd_thread()、rotate_if_needed() 记录服务消息并按编码器就绪或超时旋转分段,无相机包时有兜底旋转
    encoderd encoder_thread() 图像流编码与分段配合;日志索引和视频负载是不同文件/主题
    uploader Uploader.list_upload_files()、upload() 跳过锁定或已上传文件;计费网络限制视频上传
    deleter deleter_step() 空间不足时优先删除可回收旧分段,避开锁定目录

    日志、上传与删除是旁路服务,不应成为转向/制动控制的同步前置条件。插件若新增高频主题,还需决定是否记录、抽样、队列容量与磁盘配额——仅在 services.py 注册主题并不足以保证长期可运维。

    常见问题

    msgq 队列能保证每条消息都被消费到吗?

    不能。msgq 是可被新写入覆盖的实时队列,读者被逐出或读指针失效时会重置到当前写指针,旧消息不保证可补读。依赖每帧必达的插件应自行定义可靠传输或落盘机制。

    deviceState.started 等于车辆已被使能吗?

    不等于。started 是 manager 启停 onroad 进程的信号,与 selfdriveState.enabled 和 panda 的 controls_allowed 是不同层概念,不能相互替代。

    为什么 Params 不适合传高频数据?

    Params 每次写入都要写临时文件、fsync、加锁并原子 rename 并同步目录,面向持久配置而非高频流。高频结构化消息应走 cereal/msgq。

    插件的参数键可以随便写吗?

    不建议。Params::clearAll() 会依生命周期标志清理未登记键,插件应声明键名、类型、默认值、清理标志与迁移版本,否则可能被意外删除。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/SYSTEM_FOUNDATION_DEEP_DIVE.md(取用了控制平面与数据平面分工、启动与进程管理、消息契约与 msgq 队列、Params 生命周期,以及设备启动门与旁路服务)。
    • 项目 02 —— openpilot-research/HARDWARE_BACKEND_USB_SNAPSHOT_CONTRACT_DEEP_DIVE.md(仅用于确认硬件选择与 deviceState 的衔接位置)。

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

    更新记录

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