标签: CarParams

  • 四个开发工具的信任边界:回放导出与 CarParams 注入

    结论:四个开发工具的信任等级并不相同:print_flags.py 只读枚举,test_fw_query_on_routes.py 只读已记录固件,run_process_on_route.py 会把新旧消息合并导出,而 set_car_params.py 会写入当前参数库。前两者可分析,后两者带副作用,其中最危险的是把 CarParams 写入下放给任意插件。

    工具清单与副作用等级

    工具 源码链路 静态结论
    test_fw_query_on_routes.py 参数可为单一路段或文件路径;文件分支取每行再交 SegmentRange,未指定段则拼 /0,随后按 QLOG 读 carParams 并分别做精确/模糊固件匹配。 原主入口探针确认列表行尾换行未剥离,正则在内部 try 前抛断言并中断整批;读取器构造失败也在 try 前传播。脚本只读已记录固件,不向 ECU 发查询。
    批量统计与不匹配输出 以 dongle_id 去重,在读路段前就把它加入集合;对不匹配固件按车型、地址去重汇总,最终打印精确/未识别/模糊计数。 假消息探针确认首个路段没有 carParams 时,同设备第二路段仍被跳过;不支持的 panda 类型会提前结束,随后又报告无 CarParams。输出含 VIN 与固件字节,需视为敏感诊断数据。
    run_process_on_route.py 默认从 CONFIGS 选全部进程,再减去被排除的两个模型进程;读完整路段调用回放,只按实际产生的消息类型删除旧输入,再将新旧消息按时间合并并保存。 探针确认零回放输出会保留并重排全部旧消息,未知白名单会选出空配置但仍读取和导出原日志。文件名进程部分来自集合迭代顺序,不是稳定标识。导出不调用车型日志净化器,副本可能携带原始 CAN/VIN。
    set_car_params.py 有路段时只读第一条 qlog 中第一条 carParams;无参数时新建空白消息,只写两个纵向标志;随后为每个键新建 Params(),按三个键顺序阻塞写入。 探针确认缺少 carParams 时在任何写入前抛 IndexError;第二键抛异常时第一键已调用成功、第三键未调用。原 Python Params.put() 会丢弃底层负错误码,故底层写盘失败可能不抛异常。本层无回滚或读回,不校验平台、VIN、safety 或当前车辆身份。
    print_flags.py 遍历 BRANDS 与各平台配置标志,按标志列出车型名称。 Platforms 与归档的 CAR 枚举均未定义脚本调用的 with_flags();存在非零标志配置,非零假枚举时原主体抛 AttributeError,零标志枚举则无输出。

    run_process_on_route:替换与导出的边界

    该脚本默认从 CONFIGS 选全部进程,再减去被排除的两个模型进程,读完整路段后调用 replay_process(),只按本轮实际产生的消息类型删除旧输入,再把新旧消息按 logMonoTime 合并并 save_log()。

    假依赖主入口探针确认:零回放输出会保留并重排全部旧消息;未知白名单会选出空配置但仍读取和导出原日志。另外,文件名中进程部分来自集合迭代顺序,不是稳定标识;save_log() 不调用车型日志净化器,副本可能携带原始 CAN/VIN。因此不能以脚本名称替代对进程副作用的审查。

    回放导出还有一个隐含前提:所选进程必须在当前代码版本下可复现,否则混合出的日志既不等于原始输入,也不等于一次真实回放;这正是需要把代码版本与被替换主题一并写入导出元数据的原因。

    test_fw_query_on_routes:批量审计的失败面

    该脚本的参数可为单一路段或文件路径;文件分支用 list(open(...)) 直接取每行,再交 SegmentRange;未指定段则拼 /0,随后按 QLOG 读取 carParams 并分别做精确/模糊固件匹配。探针确认列表行尾换行未剥离,正则在内部 try 前抛断言并中断整批;读取器构造失败同样在 try 前传播。

    统计侧以 dongle_id 去重,且在读路段之前就把该键加入集合;对不匹配固件按车型、地址去重汇总,最终打印精确、未识别与模糊匹配计数。需要提醒的是:首个路段没有 carParams 时,同设备第二路段仍被跳过;不支持的 panda 类型会提前结束,随后又报告无 CarParams。这些输出包含 VIN 与固件字节,应视为敏感诊断数据。

    set_car_params:三键写入无补偿

    有路段时,脚本只读取第一条 qlog 中第一条 carParams;无参数时新建空白消息,只把 openpilotLongitudinalControl=True、alphaLongitudinalAvailable=False 写入。随后为每个键新建 Params(),按 CarParams、CarParamsCache、CarParamsPersistent 的顺序阻塞写入。

    假 Params 探针确认:缺少 carParams 时在任何写入前抛 IndexError;第二键抛异常时第一键已调用成功、第三键未调用。但原 Python Params.put() 会丢弃底层负错误码,故底层写盘失败可能不抛异常,脚本仍继续写第三键。本层没有回滚或读回,也不校验平台、VIN、safety 或当前车辆身份。另据键属性,前两者会被 manager 启动清理,CarParamsPersistent 持久化。

    键 写入时机 生命周期
    CarParams 三键顺序写入的第一键 被 manager 启动清理。
    CarParamsCache 第二键 被 manager 启动清理。
    CarParamsPersistent 第三键 持久化。

    插件化:写入权归谁

    print_flags.py 的只读枚举也提示了一件事:即便修复了缺少 with_flags() 的问题,它仍只枚举本地声明,不证明硬件或 safety 已启用。实际车型能力仍须从车型接口、DBC 与 panda safety 交叉验证。这也说明开发工具的结论天然是局部的:它们验证的是脚本自身的控制流,而不是目标车辆的参数正确性。

    探针 覆盖的契约
    标志枚举探针 核对 Platforms 与各品牌方法清单,确认缺少 with_flags() 的异常与零输出分支。
    参数注入主入口探针 验证默认两个标志、三键写入顺序、只取首个 carParams,以及第二键异常后不触发第三键。
    固件审计主入口探针 验证行尾换行、读取器构造异常、同设备去重及不支持 panda 后的状态报告。
    回放导出主入口探针 确认剔除被排除的模型进程、只替换实际产生的类型、零输出仍导出全部旧消息。

    批量固件审计应先把路段列表解析、去重与错误计数做成显式结果;精确/模糊候选只能用于平台审核,不自动修改固件库。回放导出的日志需携带输入路段、所选进程、代码版本、被替换主题、生成主题和隐私标记,避免把混合旧/新消息误当统一来源。CarParams 的写入应归受控车辆身份服务所有,要求平台/线束/DBC/safety 绑定、三键一致性门和读回验证,不能下放给任意插件。

    常见问题

    为什么 set_car_params.py 不能在车端随意试跑?

    因为它会写入当前参数库:按 CarParams、CarParamsCache、CarParamsPersistent 三键顺序阻塞写入,且本层没有回滚或读回、不校验平台/VIN/safety/车辆身份。底层写盘失败还可能因负错误码被丢弃而不抛异常。

    回放导出会混入旧消息吗?

    会。探针确认零回放输出时,全部旧消息仍会进入导出并按时间排序;脚本只按本轮实际产生的消息类型删除旧输入。导出还不调用车型日志净化器,副本可能携带原始 CAN/VIN。

    为什么固件审计脚本可能整批中断?

    因为文件分支取每行时行尾换行未剥离,SegmentRange 的正则在内部 try 之前就抛断言并中断整批;读取器构造失败也在 try 前传播,同样会中断。

    print_flags.py 能证明某车型能力已启用吗?

    不能。它只遍历本地品牌与平台配置来列出车型名称,且归档中并未定义它调用的 with_flags();即使修复,它仍只枚举本地声明,不证明硬件或 safety 已启用。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/DEV_FW_REPLAY_CARPARAMS_TOOLS_TRUST_BOUNDARY_DEEP_DIVE.md(取用了四个开发工具的入口、数据流与失败边界及探针结论)。
    • 项目 02 —— openpilot-research/PROCESS_REPLAY_VALIDATION_DEEP_DIVE.md(仅用于确认回放进程与验证的边界)。

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

    更新记录

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

  • 实验模式三层状态:UI 图标、Params 请求值与运行值为何不一致

    结论:「实验模式是否生效」在源码里其实是三个不同的状态,很容易被一个 UI 图标混淆。一是请求值 ExperimentalMode,是用户意图,多个 UI 入口都能写;二是界面值,大屏行车按钮的新图标最多保持约两秒就回到消息报告的模式;三是运行值 selfdriveState.experimentalMode,由 selfdrived 在运行时车型能力门后发布,plannerd 在收到下一次模型更新时才使用它。源码没有提供端到端「点击已被规划器接受」的事务回执。以下为固定快照 c8fb906 的静态分析,不代表模型行为或实车交互已验证。

    请求入口:五个 UI 路径写同一个参数

    请求值可以从多处写入。大屏行车按钮的 ExpButton._update_state()/_handle_mouse_release() 从 selfdriveState.experimentalMode 取当前模式,在确认且 UI 认为有纵向控制时翻转 ExperimentalMode 参数;其 _engageable 只影响绘制透明度,不参与切换允许判断。大屏驻车主页的按钮点击进入设置,展示取自 Params,不直接切换模式。

    设置页的 TogglesLayout._handle_experimental_mode_toggle() 以 UI 能力门决定可用性,首次开启需确认,确认后阻塞写两个参数,不可用时清除请求参数。mici 首页的 HomeLayout._update_state() 在长按超过 0.5 秒且 UI 能力与确认均成立时,修改 UI 本地布尔值并阻塞写 ExperimentalMode;mici 设置页首次开启走确认页面,后续写请求参数,但这里的普通 put_bool 未指定 block=True。

    阶段 代码入口 源码可确认的行为
    大屏行车按钮 ExpButton._update_state()、_handle_mouse_release() 从 selfdriveState.experimentalMode 取当前模式;确认且 UI 认为有纵向控制时翻转参数。
    大屏按钮反馈 _held_or_actual_mode() 新图标保持约两秒后回到消息报告的模式;保持期只是视觉反馈。
    大屏驻车主页 HomeLayout._setup_callbacks()、ExperimentalModeButton.show_event() 点击进入设置;展示取自 Params,不直接切换模式。
    大屏设置 TogglesLayout._handle_experimental_mode_toggle() UI 能力门决定可用性;首次开启需确认;不可用时清除请求参数。
    mici 首页 HomeLayout._update_state() 长按超过 0.5 秒且能力与确认成立时,阻塞写 ExperimentalMode。
    mici 设置 TogglesLayout._on_experimental_mode() 首次开启走确认页;后续写请求参数,普通 put_bool 未指定 block=True。

    UI 能力计算集中在 UIState.update_params():它从 CarParamsPersistent 读车型能力,若 alphaLongitudinalAvailable 则展示门取 AlphaLongitudinalEnabled,否则取 CP.openpilotLongitudinalControl,后台约每 0.2 秒刷新。也就是说「按钮能不能点」和「模式实际会不会生效」用的是两套判定依据。

    参数持久化与两个布尔键

    ExperimentalMode 与 ExperimentalModeConfirmed 都是持久化布尔键。要点在于写盘语义:Params.put_bool(..., block=True) 会等待写盘,而不带 block=True 的调用只提交写入;这两种调用都不等于下游已消费。mici 首页调用的是通用 Params.put(..., block=True),其布尔值经类型转换后写入,与 put_bool 的 API 形式不同。

    键 / 调用 含义 边界
    ExperimentalMode 用户请求值 多入口可写;无纵向控制能力时可能被清除。
    ExperimentalModeConfirmed 首次确认记录 持久化布尔键,用于首次开启确认。
    put_bool(block=True) 等待写盘 写盘完成不等于下游已消费。
    put_bool() 仅提交写入 mici 设置的普通调用属此类。

    运行值由能力门决定

    运行时一侧,UIState.update_params() 从 CarParamsPersistent 读车型能力:若 alphaLongitudinalAvailable,展示门取 AlphaLongitudinalEnabled,否则取 CP.openpilotLongitudinalControl,后台约每 0.2 秒刷新。而 SelfdriveD.__init__() 在无 openpilot 纵向能力时启动清除请求;params_thread() 约每 0.1 秒计算 ExperimentalMode AND CP.openpilotLongitudinalControl,主循环再发布 selfdriveState.experimentalMode。

    这里有一个条件性差异值得注意:UI 的能力展示读 CarParamsPersistent 且在 alpha 可用时依赖 AlphaLongitudinalEnabled,实际模式却按运行时 CP.openpilotLongitudinalControl 判定。若持久车型参数、运行时参数或刷新时点不同,UI 允许/展示与实际激活可短暂不一致;静态阅读不能证明这种差异会在某辆车上发生。

    规划消费与规划激活

    plannerd.main() 与 LongitudinalPlanner.update() 随 modelV2 更新读取 selfdriveState;实验模式改变巡航加速度计算,并把模型 desiredAcceleration 加入候选,最终按候选的最小加速度选目标——不是「模型始终直接控制油门刹车」。下游 controlsd 从 longitudinalPlan.aTarget 经纵向控制器生成候选执行器输出,card 还读取参数作为启用时巡航初值的输入,二者各有各自的更新时序。

    面向自制硬件与插件重构

    把 UI 插件的操作定义为 ModeChangeRequest,携带请求 ID、来源、单调时间戳和车辆能力版本;只授权写请求,不授予其直接发布 selfdriveState、longitudinalPlan 或 CAN 的能力。由固定核心层发布 requested / rejected / accepted / active 等结构化状态,其中 active 必须来自规划器消费确认,不能用图标、Params 写盘或 selfdriveState 单独代替;当前源码尚无此回执协议。

    能力门应以运行时 CarParams 与独立网关安全策略为权威,UI 可显示能力但其缓存不得成为执行许可。验证顺序上,先用回放验证参数切换、状态发布、规划候选选择及失联时序,再对目标车型验证 controlsd 活动门、card 输出与 panda 安全钩子。

    常见问题

    点了实验模式按钮,规划器就立刻用上了吗?

    不一定。按钮翻转的是请求值 ExperimentalMode,运行值 selfdriveState.experimentalMode 由 selfdrived 在能力门后发布,plannerd 要等下一次模型更新才使用;源码没有端到端「点击已被规划器接受」的事务回执。

    大屏上的新图标表示模式已切换成功吗?

    不能这样理解。_held_or_actual_mode() 里的新图标最多保持约两秒,然后回到消息报告的模式;这段保持期只是视觉反馈,不是参数写盘或规划激活的回执。

    put_bool(block=True) 写盘成功就说明下游已消费吗?

    不等于。block=True 只是等待写盘完成,不带 block=True 的调用只提交写入;两种调用都不代表下游已消费,mici 设置页用的就是不带 block 的普通 put_bool。

    为什么 UI 显示的可用性与实际激活会不一致?

    因为判定依据不同:UI 能力展示读 CarParamsPersistent、在 alpha 可用时依赖 AlphaLongitudinalEnabled,实际模式按运行时 CP.openpilotLongitudinalControl 判定。持久参数、运行时参数或刷新时点不同就可能短暂不一致,但不能据此断定会在某辆车上发生。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/EXPERIMENTAL_MODE_UI_PARAM_ACTIVATION_DEEP_DIVE.md(取用了五个 UI 请求入口、两个持久化布尔键、三层状态划分、运行值能力门与规划消费说明)。

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

    更新记录

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

  • process_replay 容器:初始 Params、消息合流与合成时钟

    结论:回放器的关键是分清「测试初始条件」与「真实运行」,以及分清「合成时钟」与「真实时延」。replay_process()` 先用 migrate_all() 迁移历史消息,再交给多进程调度;return_all_logs=True 时一旦某主题产生至少一条新输出,该主题全部旧消息被替换。「完整日志」不是原始日志加增量。同时 get_output_msgs() 把输出时间改成 start_time + processing_time,这是调度模型的合成值,不是进程真实运行时间、相机曝光时间或 CAN 发送时间。

    一、入口和数据所有权

    入口/对象 行为 重要边界
    replay_process_with_name() 名称经 get_process_config() 转成独立配置,再委托 replay_process()。 名称必须存在于 CONFIGS,不是 manager 的 44 个进程均可回放。
    replay_process() migrate_all() 先做历史 schema/manager/panda/相机状态准备,再调用多进程调度;return_all_logs=True 时删除迁移日志中所有与新输出同主题的旧消息,合并后按 logMonoTime 排序。 迁移会改变输入和时间顺序;同主题旧消息被整体替换,而非增量叠加。
    ProcessContainer.__init__() 创建随机 OpenpilotPrefix,深拷贝回放配置和受管进程对象,维护输入队列/计数/最近输入时间。 前缀隔离 Params、msgq、部分日志路径,不隔离 CPU、设备或权限;同一解释器内环境变量仍是进程级共享状态。
    ProcessContainer.start() 显式创建前缀目录,写 Params/环境,运行配置回调,开启 fake 事件,创建 Pub/Sub 和可选 VisionIPC,然后启动进程并运行初始化回调。 先启动后回调意味着初始化回调必须按 fake 事件握手;启动中途异常可能留下部分资源。
    ProcessContainer.run_step() 先用触发器决定周期是否结束,当前消息始终入 msg_queue;周期结束才收上周期输出、依次发布队列消息与相机帧、解锁 fake 事件。 输出采集发生在本周期输入发送之前,不能按单次调用直接对齐输入输出。
    ProcessContainer.stop() 对进程发 SIGKILL、关闭 fake 事件、删除前缀目录及回放设置的环境变量。 不恢复回放前相同变量的旧值;失败清理可能再抛异常并掩盖最初故障。

    二、初始 Params、指纹与环境

    生成函数 关键写入 语义
    generate_params_config() 默认开启 OpenpilotEnabledToggle/DisengageOnAccelerator,默认不禁用日志;含 ubloxGnss 写 UbloxAvailable,含 driverMonitoringState 写 IsRhdDetected;固件来源保存 CarParamsCache,纵向权限与 notCar 决定 AlphaLongitudinalEnabled、JoystickDebugMode。 这些是测试初始条件,不是重新判断车辆安全许可。
    generate_environ_config() 设置 PARAMS_ROOT 和 REPLAY=1,可选 LOG_ROOT;强制指纹时设 SKIP_FW_QUERY=1。 原函数的 AST 隔离探针分别确认三种字典输出,未创建任何目录或执行指纹查询。
    _setup_env() / _clean_env() 对空字符串执行「从 os.environ 删除该键」,非空值覆盖;设置 PROC_NAME;SIMULATION=1。 _clean_env() 不会恢复之前的值,故回放应运行于独立子进程或自行保留环境快照。

    环境隔离还有一层容易被忽略:_setup_env() 对配置中的空字符串执行「从 os.environ 删除该键」,非空值直接覆盖,并设置 PROC_NAME;_clean_env() 删除配置中所有环境键及 PROC_NAME/SIMULATION,不会恢复之前的值。而 OpenpilotPrefix 自己仅保存和恢复 OPENPILOT_PREFIX。因此在已有环境变量的测试宿主中,回放应运行于独立子进程或自行保留环境快照,不能把容器当成可嵌套的完整环境事务。

    OpenpilotPrefix 自己仅保存和恢复 OPENPILOT_PREFIX;因此在已有环境变量的测试宿主中,容器不是可嵌套的完整环境事务。

    三、消息合流和时钟

    _replay_multi_process() 先创建所有容器,再计算 all_pubs(注入主题)与 all_subs(输出主题)。仅把原路段中属于 all_pubs - all_subs 的消息放入外部队列;与某个受测进程输出同主题的原日志不会进入外部队列,必须依靠新的内部输出驱动下游。每次被测进程产出的消息都加入最终 log_msgs;若其主题又属于 all_pubs,则加入内部队列和按 logMonoTime 排序的小根堆。外部和内部候选比较时,时间相等优先内部消息。

    时间上,get_output_msgs() 等进程下一次 recv_called,排空输出 socket,并把所有采到的输出时间改成 start_time + processing_time;start_time 是此前收到的输入最大 logMonoTime。run_step() 内的 Timeout 只覆盖已结束的周期;没有触发周期的积压输入,以及尾部 get_output_msgs() 冲刷,不共享这个超时上下文。

    两个收尾条件值得单独验证:主循环在外部队列耗尽、所有容器的 msg_queue 都为空时会停止,即使内部堆仍有已生成消息——这些消息可能已进入最终输出,却未再次送给下游容器;结束后逐容器冲刷输出也只追加到最终结果,不重新进入内部堆。这是源码控制流可见的覆盖边界,尚无实际多进程路段复现,不能把多进程回放等同完整闭环执行。

    四、故障与硬件插件验收

    失效路径也需注意:_replay_multi_process() 先将容器加入 containers 再调用 start(),若在 self.rc 或 capture 初始化之前抛错,finally 中的 stop() 及捕获读取可能因部分初始化而再次异常。尾部冲刷的 ReplayContext.wait_for_recv_called() 最终使用默认无限等待,需另设总 deadline 才能证明崩溃/失联路径有界。以上为静态失效路径,未执行故障注入。

    要把新的传感器、计算或车辆接口做成插件,需给回放器明确的输入/输出主题、触发条件、初始 Params/环境、时钟域、消息新鲜度、最大积压和异常清理契约。验收应分开记录功能输出、端到端时延、处理器资源、断流/进程退出和 panda 安全测试;processing_time 或对齐后的参考日志不构成实车时限证据。当前缺 capnp、参考路段和目标硬件,本专题没有运行完整回放。此外迁移本身会改变输入与时间顺序,因此比较两次回放前必须固定同一迁移版本与路段快照,否则结果不可比。

    常见问题

    所有 manager 进程都能被回放吗?

    不是。replay_process_with_name() 要求名称存在于 CONFIGS,源文档明确不是 manager 的 44 个进程均可回放。名称只是转成独立配置的入口,不代表该进程具备完整的回放输入/输出契约。

    return_all_logs=True 时日志是原日志加增量吗?

    不是。一旦某主题产生至少一条新输出,该主题全部旧消息被替换,而不是叠加。源文档强调「完整日志」不是原始日志加增量,且迁移会改变输入和时间顺序。

    processing_time 是进程真实运行时间吗?

    不是。get_output_msgs() 把输出时间改成 start_time + processing_time,start_time 是此前输入最大 logMonoTime,因此它是调度模型的合成值,不是进程真实运行时间、相机曝光时间或 CAN 发送时间。

    多进程回放等于完整闭环吗?

    不等于。主循环在外部队列耗尽且所有容器 msg_queue 为空时会停止,即使内部堆仍有已生成消息;尾部冲刷也不重新进入内部堆。源文档称这是控制流可见的覆盖边界,尚无实际多进程路段复现。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PROCESS_REPLAY_CONTAINER_SCHEDULER_PARAM_LIFECYCLE_DEEP_DIVE.md(取用了容器入口与数据所有权、初始 Params/环境生成、消息合流与合成时钟、故障与插件验收)。

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

    更新记录

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

  • Params 生命周期:注册键、清理时机与未上抛的写失败

    结论:Params 是低频配置与状态的持久层,不是高频消息总线,也不是独立的车辆安全授权。每个合法键都有注册类型、位标志和可选默认值;同名相近的键(CarParamsCache / CarParamsPersistent / CarParamsPrevRoute)用途与新鲜度完全不同。写入路径存在一个明确的缺口:文件系统返回的负错误码没有在 Python put() 中被检查,所以关键安全配置不能仅凭 put(..., block=True) 没抛异常就宣称持久化成功。固定快照基线为 c8fb906,本地归档缺编译后的 libparams_c,未运行原生参数测试或断电故障注入。

    注册、读写与类型

    params_keys.h 与 ParamKeyAttributes 规定每个合法键有类型、位标志、可选默认值。例如 CarParams 是 bytes 且在 manager 启动 / 上路时清除,CarParamsPersistent 则持久保存。动态插件不能只在自己进程里声明任意新键;必须设计命名空间、类型、迁移与生命周期,并纳入可信配置注册机制。

    Params.check_key() 让 Python 拒绝未注册键及含 NUL 的键;put() 仅接受与注册类型匹配的 Python 类型,get() 按声明反序列化。Python ParamKeyType 支持 string、bool、int、float、time、JSON、bytes;C++ Params 是字节存储,并不自行验证每个键的业务内容。get_bool() 只把 "1" 视为真,其余值都返回 false,因此它不能用来判断某键是否被明确写入;get() 与 get_bool() 在缺省处理上也不完全相同。

    组件 / 函数 已核对契约 对插件的影响
    params_keys.h / ParamKeyAttributes 每个合法键有类型、位标志、可选默认值;CarParams 启动 / 上路清除,CarParamsPersistent 持久。 插件须设计命名空间、类型、迁移与生命周期。
    check_key() 拒绝未注册键与含 NUL 的键;put() 只接受匹配类型。 不能靠自由字符串文件模拟已登记参数。
    get() / _cpp2python() 非阻塞空字节不请求默认时返回 None,请求默认时用注册表默认值;block=True 得到空字节时抛 KeyboardInterrupt;损坏 JSON 警告一次并尝试默认。 默认值回退可能掩盖原始损坏;空字符串不能区分「缺失」与「已写空值」。
    put() / params_put() 默认走 C++ 异步队列;block=True 直接调用文件写入;负返回码未在 Python 侧检查。 关键配置不能只凭无异常宣称持久化成功。

    存储层:原子替换与异步写队列

    Params::put() 走临时文件写入、fsync、目录锁、原子 rename、目录 fsync;异步写线程按队列逐项执行,析构时等待。单键替换具备原子写入路径,但多键配置不是原子事务;异步写也不是跨进程的严格提交确认。

    参数目录通过参数前缀软链接暴露,OpenpilotPrefix 能让测试进程隔离到单独前缀;C ABI 层负责异常翻译与返回缓冲复制。这两层保证的是「路径与接口」的正确性,并不校验业务内容是否符合调用方预期。

    Params::clearAll() 按位标志删除匹配键,同时删除注册表之外的文件;OpenpilotPrefix 能把测试进程隔离到单独前缀。这意味着未登记的插件键可能在清理时消失,验证环境也必须记录前缀与参数快照。

    manager 的清理时机与实际键例

    参数的新鲜度由 manager 在几个时点统一管理:

    时点 源码动作 示例影响
    manager 初始化 清除 CLEAR_ON_MANAGER_START、上路、下路、点火标志;发布渠道还清 DEVELOPMENT_ONLY,随后给缺失的有默认值键写默认。 CarParams、ControlsReady、FirmwareQueryDone 被清除;OpenpilotEnabledToggle 等持久键保留 / 补默认。
    上路边沿 检测 deviceState.started 从 false 到 true,清 CLEAR_ON_ONROAD_TRANSITION。 车型参数需 card 在新行车周期重新提供,插件不能缓存旧控制授权。
    下路边沿 清 CLEAR_ON_OFFROAD_TRANSITION,并更新 IsOffroad。 调试模式、Chestnut 活动状态不应跨周期视为有效。
    点火边沿 基于 pandaStates 点火值清 CLEAR_ON_IGNITION_ON。 DriverTooDistracted、ChestnutActive 等按点火周期复位。

    安全敏感的 SecOCKey 标为 PERSISTENT | DONT_LOG,而 DONT_LOG 的含义由 logger 实现为保留键名但不写值。它不是加密、访问控制或对所有外部插件日志的自动脱敏。

    失败路径与验证

    params_c.cc 只将抛出的 C++ 异常转成 last_error;params_put() 原样返回 Params::put() 的整数结果,而 Params.put() 丢弃该结果。原方法探针确认:若假 C 函数返回负值而 last_error 为空,_bind() 会把负值交还给 Python,两个写入方法都返回 None 且不抛异常;若 last_error 有值,_bind() 抛 RuntimeError。mkstemp、write、fsync 或 rename 的真实磁盘故障尚未复现。

    Params::clearAll() 对删除操作不检查每个 unlink 的返回值;FileLock 锁打开或加锁失败时记录错误,但构造并未抛异常。配置冻结与清理的失败检测不能仅依赖调用返回。现有参数测试覆盖同步 / 异步读写、未知键、默认值与按标志清理,日志测试检查 DONT_LOG 值被遮蔽;本次未运行。新插件参数还应测试断电、磁盘满、跨进程竞争、旧版本迁移、上路 / 下路与点火边沿,以及记录中是否泄露密钥。插件注册器应在 offroad 冻结配置版本、哈希及权限;行车进程读到的配置与 panda 固件安全模式必须可互相校验,失败时保持被动模式。

    常见问题

    put(block=True) 没报错,是否说明参数已经落盘?

    不能这样推断。文件系统调用返回的负错误码没有在 Python put() 中检查;探针确认假 C 函数返回负值且 last_error 为空时,写入方法仍返回 None 且不抛异常。关键安全配置需读回、版本校验及故障注入。

    CarParams 和 CarParamsPersistent 有什么区别?

    用途与新鲜度不同。CarParams 是 bytes 且在 manager 启动 / 上路时被清除,需要 card 在新行车周期重新提供;CarParamsPersistent 与 CarParamsPrevRoute 是持久键。名称相似但不能互相替代。

    DONT_LOG 会自动脱敏日志吗?

    不会。DONT_LOG 由 logger 实现为保留键名但不写值;它不是加密、访问控制,也不是对所有外部插件日志的自动脱敏。安全敏感的 SecOCKey 只是被标为 PERSISTENT | DONT_LOG。

    插件能自己声明新的参数键吗?

    不能只在自己进程里声明。合法键必须在注册表里有类型、位标志与默认值;check_key() 会拒绝未注册键。clearAll() 还会删除注册表之外的文件,所以未登记的插件键可能在清理时消失。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PARAMS_LIFECYCLE_DEEP_DIVE.md(取用了键注册、读写类型、存储原子性、manager 清理时机与写失败路径分析)。

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

    更新记录

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

  • 设置页写了参数不等于生效:ADB、alpha 纵向与总开关的下游链差在哪

    结论:车载设置页的开关只做一件事——往 Params 写值并(在少数情况下)请求一次行车周期;参数写进去不等于硬件行为已经验证,UI 显示门也不是安全授权。软件更新页的「检查/下载」是发给更新器的信号请求,安装按钮只写 DoReboot;开发者页的 ADB/SSH、动作模式与 alpha 纵向涉及不同下游消费者;驾驶开关则通过 card、selfdrived 影响使能。大屏与 mici 两套界面在时间门、异步调度与锁键语义上并不完全对齐。以下为固定源码快照 c8fb906 的静态分析。

    软件更新页:请求不是直接安装

    大屏 SoftwareLayout 从 Params 显示当前描述/发行说明、UpdaterState、失败次数、可拉取/可安装状态与目标分支。_update_state() 只在 offroad 显示「检查/下载」和「安装」,且安装可见还要求 UpdateAvailable。按钮点击后的约 10 秒等待窗只处理「更新器未响应」的 UI 状态,不保证下载成功。

    _on_download_update() 依据按钮文案选择 SIGUSR1 检查或 SIGHUP 拉取;_on_select_branch() 写 UpdaterTargetBranch 后发送检查信号。WaitTimeHelper 把 SIGUSR1 转成 CHECK、SIGHUP 转成 FETCH,由更新器负责分支解析与 staging/finalizing,再写 UpdaterFetchAvailable、UpdateAvailable 等 Params。两屏卸载都经确认后写 DoUninstall;manager_thread() 检出 DoUninstall/DoReboot/DoShutdown 后退出循环,由 main() 调用平台卸载或重启。实际固件切槽/卸载动作仍需平台级验证。

    开发者页:参数到运行链

    开发者页的控件覆盖 ADB/SSH 开关、SSH 公钥拉取、joystick 与纵横向动作模式、alpha 纵向控制确认以及 UI FPS/触摸调试。它们大多只写一个 Params 键,是否会真正改变硬件行为取决于设备系统层或进程表:例如 ADB 控件 offroad 禁用,而 SSH 开关未使用相同的 offroad 门;动作模式三者在 UI 回调中互斥写 Params,但参数可由其他进程直接修改,UI 互斥不是进程表的全局不变量。

    SSH 公钥走 SshKeyFetcher:请求 GitHub 的 <username>.keys,把用户名与返回文本写 GithubUsername/GithubSshKeys,出错时清理;这是双键非原子写,系统级 authorized_keys 生效链仍需平台审计。

    控件 下游或边界
    ADB / SSH 写 AdbEnabled、SshEnabled;服务实际启停取决于设备系统层。ADB 控件 offroad 禁用,SSH 未使用相同门。
    SSH 公钥 请求 GitHub <username>.keys,写 GithubUsername/GithubSshKeys;双键非原子写,错误时清理。
    joystick / 纵向 / 横向动作 三者在 UI 回调中互斥写 Params;process_config.py 据参数选调试进程,参数可被其他进程改。
    Alpha 纵向控制 非 release、车型 alphaLongitudinalAvailable、确认页之后写 AlphaLongitudinalEnabled 并请求行车周期;可能改变原车 ACC/AEB 组合。
    UI 调试 写 ShowDebugInfo 并在本 UI 进程切换触摸点/FPS 叠加,不影响 CAN 安全许可。

    行车周期与控制模式

    restart_needed_callback() 写 OnroadCycleRequested。hardwared 消费后清零,并在一段时间内使 not_onroad_cycle 条件为假,迫使 started 周期重建——这不是操作系统硬重启。新设备需要测试「参数写入 → deviceState.started 边沿 → manager 进程重选 → card 车型参数重建 → panda 安全模式重新配置」的顺序。

    这里有一个两屏不一致点值得注意:大屏动作模式回调写互斥参数,但并未像 mici 那样对纵/横向动作模式调用 restart_needed_callback(),两屏的即时生效路径可能不同。因此不能从「UI 控件已改变」推断当前控制进程已安全切换,需要通过回放或实机验证 manager 对参数变化的读取节奏。

    驾驶开关与消费关系

    TogglesLayout 定义 OpenpilotEnabledToggle、ExperimentalMode、加速踏板退出、LDW、Always-On DM、座舱摄像头/麦克风记录及单位切换。它按车型 CarParams 是否有 openpilot 纵向控制动态限制实验模式与驾驶风格;实验模式首次启用要求确认,涉及重启的开关在 engaged 时禁用。

    消费侧,card 在初始化时结合 OpenpilotEnabledToggle、控制器是否存在与车型 dashcamOnly 决定 CP.passive;selfdrived 与 card 的运行状态读取实验模式。LongitudinalPersonality 还可通过车辆距离按钮变化并反映到 UI,因此 UI 选项不是唯一写入者。两套页面的限制也不等价:大屏遍历各开关的 *Lock 参数,mici 在当前页面只显式处理 RecordFrontLock,移植配置策略时须逐键核对。

    对比项 大屏 SoftwareLayout / TogglesLayout mici 对应界面
    更新请求发送 直接发 SIGUSR1/SIGHUP,约 10 秒等待窗 后台线程发送,系统时间无效时阻止按钮请求
    动作模式重启 写互斥参数,未调用 restart_needed_callback() 对纵/横向动作模式调用 restart_needed_callback()
    锁键处理 遍历各开关的 *Lock 参数 当前页面只显式处理 RecordFrontLock

    自制硬件与插件化建议

    源文档给出一组建议:把设置描述为「能力 + 策略 + 执行者」,UI 插件只呈现能力,更新器/进程管理器/平台服务执行请求;给更新与危险操作定义结构化请求及结果,而非让多个 UI 复制 pkill、布尔 Params、10 秒本地计时;保留 offroad/engaged 二次校验,确保从外部 API 调用也无法绕过。

    对 alpha 纵向、动作模式、openpilot 总开关,应采用车型能力声明和独立安全审核;插件不得替换 panda 的 TX/RX 安全策略,硬件安全默认态仍为失效安全。所有这些端到端行为(ADB/SSH 真实启停、公钥更新原子性、下载中断与升级回滚、断电恢复)都需在目标板上验证,本文只证明代码路径存在。

    常见问题

    点了「安装」是不是就完成更新了?

    不是。两屏的安装按钮最终都写 DoReboot,并非在 UI 里直接把 overlay 写入当前分区;真正的前台切换由启动脚本完成。UI 的检查与下载也只是请求语义。

    改了 ADB 或 SSH 开关就一定能连上设备吗?

    不一定。源码快照只能确认参数被写入 AdbEnabled/SshEnabled,服务实际启停取决于设备系统层;而且 ADB 控件 offroad 禁用,SSH 开关未使用相同的 offroad 门。

    动作模式切换为什么有时要重启?

    因为 restart_needed_callback() 会写 OnroadCycleRequested,由 hardwared 触发一次行车周期重建(不是操作系统硬重启)。大屏动作模式未调用该回调,mici 调用了,两屏即时生效路径可能不同。

    UI 里的实验模式和驾驶风格为什么有时不显示?

    因为 TogglesLayout 按车型 CarParams 是否有 openpilot 纵向控制动态隐藏实验模式与驾驶风格;没有纵向能力的车型看不到这两项,实验模式首次启用还要求确认。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/SOFTWARE_DEVELOPER_SETTINGS_DEEP_DIVE.md(软件/开发者/驾驶开关三页的 UI 函数、Params 与下游消费者)。
    • 项目 02 —— openpilot-research/ 同目录探针记录:更新请求信号、行车周期重建与两屏锁键差异的静态核对结论。

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

    更新记录

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

  • 17 个品牌参数怎么构建:CarParams 冻结只到顶层,嵌套字典仍可变

    结论:车型参数不是一张静态表,而是「17 个品牌枚举 → PLATFORMS 候选字典 → CarSpecs 尺寸 + PlatformConfig 文档与 DBC → 品牌 _get_params() 覆写」的多段构建结果。冻结(freeze)只作用于配置对象这一层:flags 被锁定后不可改,但嵌套的 dbc_dict 与 car_docs 仍可原地修改,override() 返回的还是未冻结副本。因此「能生成 CarParams」只说明字典装载成功,既不等于该车型被识别,也不等于控制器获准发送。以下为固定源码快照 c8fb906 的静态结构分析。

    参数构建的顺序与产物

    values.py 汇集 17 个品牌的 CAR 枚举,并生成候选名到平台对象的 PLATFORMS 字典。每个平台带着三样东西:描述整车尺寸的 CarSpecs、记录文档与 DBC 映射的 PlatformConfig,以及一组 flags。枚举构造时给配置填 platform_str 并冻结配置对象;CarSpecs 本身是 frozen dataclass。

    真正把参数拼起来的是两段函数。get_std_params() 先写入车型名、横向最大加速度文档值、控制类型、轮速系数、PCM 巡航与纵向缺省;随后 get_params() 复制平台规格并调用品牌 _get_params()。这个钩子可以依据 CAN 指纹、固件、开发/发布模式设置总线、雷达、纵向与控制调参,是品牌策略的主要落点。

    品牌覆写之后,普通汽车会加 STD_CARGO_KG=136 kg,再按以 Civic 为基准的比例公式估计 rotationalInertia 与 tireStiffnessFront/Rear;notCar 跳过载荷。这里要强调:刚度与惯量是启发式缩放,不是本次车辆的在线辨识结果,自制硬件接入车型时这些动力学参数不保证准确。

    阶段 入口 产出或作用
    平台注册 values.py / PLATFORMS 17 个品牌候选名映射到平台对象与 PlatformConfig。
    共同缺省 get_std_params() 写入车型名、控制类型、轮速系数、巡航与纵向缺省。
    品牌覆写 品牌 _get_params() 按指纹/固件/模式设置总线、雷达、纵向、panda 安全配置。
    载荷与动力学 STD_CARGO_KG 与缩放公式 非 notCar 加 136 kg,估计转动惯量与轮胎刚度。

    冻结边界:顶层不可变,嵌套仍可变

    隔离探针确认了一个容易被误读的边界:创建 Platforms 子枚举时,PlatformsType.__new__() 给配置写入平台名并冻结,随后直接改 flags 会抛异常;但 dbc_dict 的条目与 car_docs 列表仍可原地修改。

    更关键的是 override() 的语义。对已冻结配置调用 override(flags=...) 会通过 dataclasses.replace() 产生一个未冻结的新对象,且新旧配置共用同一 dbc_dict 引用;相对地,CarSpecs.override() 返回的新规格仍是 frozen dataclass。也就是说,「冻结的是配置对象属性」不能被误解为整个嵌套对象图都经过不可变性与签名校验。自制插件若用 manifest 描述车型,需把嵌套映射与列表做不可变快照或深拷贝并纳入签名与哈希,不能只依赖顶层 freeze()。

    扭矩数据与横向回调

    横向能力由三份 TOML 与一组回调共同决定。get_torque_params() 读取 substitute.toml、params.toml、override.toml,以 legend 解释三列数值,结果缓存在进程内;同一候选若直接出现在多个 TOML 会抛异常,替代目标则按 override 优先、params 次之取值。configure_torque_tune() 设置 FRICTION、LAT_ACCEL_FACTOR、零 offset 与角度死区。

    缺省前馈 get_steer_feedforward_default() 约为目标转角乘车速平方,GM 等品牌可重写。缺省的 torque_from_lateral_accel_linear() 与 lateral_accel_from_torque_linear() 按 latAccelFactor 正反映射,品牌同样可替换。要注意 MAX_LAT_ACCEL_MEASURED 用于 CarParams.maxLateralAccel,而摩擦与侧向加速度因子进入扭矩调参,三者不能互换。

    源或函数 实际作用 关注点
    get_torque_params() 读取三份 TOML 并缓存结果 同候选出现在多个 TOML 会抛异常;替代目标按 override 优先。
    configure_torque_tune() 设置摩擦、侧向加速度因子、零 offset 与角度死区 数据表与平台覆写共同决定控制器输入,不能假设热改立即生效。
    get_steer_feedforward_default() 缺省前馈约为目标转角乘车速平方 GM 等品牌可重写,不是跨车型通用标定。
    torque_from_lateral_accel_linear() 按 latAccelFactor 正反映射扭矩与侧向加速度 需验证符号、单位、摩擦与零点。

    实例运行契约与基类扩展点

    CarInterfaceBase.__init__() 创建品牌 CarState、其 CANParser 集合与 CarController,并把解析器的 DBC 名传给控制器。update() 更新所有解析器、调用品牌状态函数、汇总 canValid/canTimeout 并处理仪表速度与按键;apply() 委派品牌控制器生成回报与待发 CAN 帧,但它不负责硬件限幅。

    仓储里还有几个常被忽略的基类语义:RadarInterfaceBase.update() 缺省每五次调用返回空雷达数据、其余返回 None;init()/deinit() 供品牌实施 ECU 初始化或恢复,基类为空,且 deinit() 在 card 退出时不保证被调用;get_interface_attr() 动态导入品牌 values/fingerprints 属性,ImportError/OSError 会被静默跳过,因此它并不是可靠的插件装载审计。

    基类接口 职责与边界
    __init__() 创建品牌 CarState、CANParser 与 CarController;DBC 名传给控制器。
    update() 更新解析器、汇总 canValid/canTimeout、处理仪表速度与按键。
    apply() 委派品牌控制器生成回报与待发帧,不负责硬件限幅。
    RadarInterfaceBase.update() 缺省每五次返回空雷达数据,其余返回 None,有雷达平台须覆写。
    init()/deinit() 品牌可实施 ECU 初始化或恢复;deinit() 不保证在 card 退出时被调用。
    get_interface_attr() 动态导入品牌属性,异常静默跳过,非可靠装载审计。

    插件化边界与验证缺口

    面向自制硬件,源文档给出的是一组建议设计而非已实现能力:平台插件以版本化 manifest 声明车型、硬件拓扑、DBC 哈希、固件证据、单位、参数范围与所需安全模式;参数构建拆成纯函数与受限生命周期钩子,_get_params()、init() 不应由普通诊断或 UI 插件获得原始 CAN 发送权;行车期间冻结参数与插件版本,重启后再重新识别与验证。

    同时应区分四个层级的门:「可生成 CarParams」「CANParser 正常」「控制器允许发送」「MCU 允许执行」;任一失败都应退回只读或 noOutput。需要提醒的是,现有测试只覆盖参数基本范围与空 CAN 下的不崩溃行为,不覆盖真实车型识别、DBC 真实性、全部 _get_params() 分支与实车动力学。自研硬件仍需要离线日志、台架与实车的分级验证。

    常见问题

    冻结后的 PlatformConfig 真的完全不能改吗?

    不是。冻结只锁住配置对象属性:flags 不可改会抛异常,但嵌套的 dbc_dict 条目与 car_docs 列表仍可原地修改。override() 还会通过 dataclasses.replace() 生成未冻结新对象,并且新旧配置共用同一 dbc_dict 引用。插件若依赖不可变性,需自行做深拷贝并纳入签名。

    STD_CARGO_KG 是 136 kg 吗,为什么有的车型不加?

    源文档给出 STD_CARGO_KG=136 kg,普通汽车在品牌覆写后加上,notCar 跳过载荷。转动惯量与轮胎刚度也是按以 Civic 为基准的比例公式估计的启发式缩放,不是本次车辆的在线辨识结果。

    get_non_essential_params() 能当作上车参数吗?

    不能。它用空 CAN 指纹、空固件及关闭的特性开关构建参数,自身文档明确警告缺少真实 FW/指纹时控制必需参数可能不完整或错误,适合文档、测试与离线展示,不是上车控制参数的证明。

    同一个候选车型出现在多个扭矩 TOML 会怎样?

    会抛异常。源文档说明:同一候选若直接出现在多个 TOML 会抛异常;被替换的目标则按 override 优先、params 次之取值。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CAR_PLATFORM_PARAMS_CONTRACT_DEEP_DIVE.md(参数构建顺序、冻结边界、扭矩 TOML 与基类实例契约)。
    • 项目 02 —— openpilot-research/ 同目录探针记录:四项平台配置冻结契约与假平台枚举隔离探针结论。

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

    更新记录

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