标签: 纵向规划

  • 实验模式三层状态: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

  • maneuversd 纵向动作状态机:为什么第 61 次循环才激活

    结论:maneuversd 是 openpilot 中一个实验性的纵向动作发布进程,它用 DT_MDL=0.05 秒按主循环次数推进动作,而不是按真实秒数。它的就绪门要求车速偏离初速小于 0.3 m/s 且纵向处于活动状态,连续超过 60 次合格循环后第 61 次才置为 _active;进入活动后若 longActive 变假,函数仍会继续推进当前动作帧。它替换的是 plannerd 的计划发布者身份,不替换 controlsd、card 与 panda 的执行门。本文为固定源码快照 c8fb906 的静态阅读,未连接车辆与硬件。

    进程身份:发布者切换不等于安全许可

    在进程配置里,plannerd 与 maneuversd 通过条件互斥:plannerd 使用 not_long_maneuver,maneuversd 使用 long_maneuver,后者要求 started 为真且 LongitudinalManeuverMode 处于开启状态。这是 manager 正常管理路径上的发布者切换,并不是一份独立的安全许可。

    main() 先等待 CarParams 存在,再轮询 modelV2,依次取用内置的七个 Maneuver。内置计划固定 allowBrake=True、allowThrottle=True、hasLead=True、speeds=[0.2],并按 should_stop(v_ego, accel) 设置停止位;这些是实验消息的构造字段,不能代表正常纵向规划器的前车识别、速度轨迹或 FCW 能力。

    入口 源码事实 边界
    process_config.py 进程配置 plannerd 用 not_long_maneuver,maneuversd 用 long_maneuver 是 manager 正常路径的发布者切换,不是独立安全许可
    Action.__post_init__() 只断言加速度与时间节点数量一致 未校验节点非空、单调、有限、加速度范围或持续时间
    main() 等 CarParams,轮询 modelV2,每轮发三路主题 未在状态推进前用 all_checks() 做门控

    就绪门与「第 61 次循环」的由来

    Maneuver.get_accel() 在车速偏离初速小于 0.3 m/s、longActive 为真且巡航不处于 standstill 时累加就绪计数;当初速接近零时,还额外要求 carState.standstill 为真。它把状态机推进建立在「连续满足条件」的计数上,而不是单次采样。

    只有 _ready_cnt > 3/DT_MDL 才置 _active=True。由于 DT_MDL=0.05,3/DT_MDL 等于 60,而这里用的是严格大于,所以至少需要 61 次合格循环,而不是精确的 3.00 秒墙钟。未进入活动状态时,函数返回一个限幅到 ±2 m/s² 的初速追赶目标。

    条件 阈值 说明
    车速偏离初速 小于 0.3 m/s 偏离越大越不计入就绪计数
    纵向激活 longActive 为真 初速接近零时还要 carState.standstill
    就绪计数门 _ready_cnt > 3/DT_MDL 严格大于,至少 61 次合格循环
    未活动追赶目标 ±2 m/s² 限幅后的初速追赶,不是最终执行命令

    动作插值、重复与 reset 边界

    Maneuver._step() 按 _action_frames * DT_MDL 对当前 Action 插值;帧计数加一后,只有超过末节点时间才切换到下一个动作、重做或结束,因此最后一个时间节点需要再推进一帧才会切换。以主循环次数近似时间,意味着卡顿或模型节拍异常都会改变墙钟持续时长。

    repeat=2 使内置动作总共执行三轮。reset() 只清 _active、_action_frames、_action_index,不清 _ready_cnt、_repeated、_finished;它与横向子类的 reset() 不同,不能当作完整的状态重置。若 repeat 之后就绪条件继续成立,下一循环可以立即再次激活。

    三路发布与有效位语义

    每轮 main() 依次发出 alertDebug、longitudinalPlan 和 driverAssistance。计划的 valid=sm.all_checks(),另两路直接为真;其中 driverAssistance 是空默认字段,不提供常规 LDW 结果。动作列表耗尽后,仍会按同样顺序持续发送 aTarget=0 的计划和完成提示。

    源码没有在进入 get_accel() 之前用 sm.all_checks() 阻止状态推进,因此无效计划也会先完成计算并填入目标,只是把外层有效位标为假。

    主题 有效位 内容 边界
    longitudinalPlan sm.all_checks() 按动作插值填入 aTarget、停止位等 有效位不冻结状态机
    alertDebug 恒为真 纵向动作模式告警 由 selfdrived 消费,不做执行门
    driverAssistance 恒为真 空默认字段 不提供常规 LDW 结果

    失效路径与插件化要求

    SubMaster.update() 的默认 100 ms 超时也会让循环继续,所以「循环次数 × DT_MDL」并不等于真实经过的时间。sm.all_checks() 只设置发布事件的有效位,不冻结状态机;本进程本身没有「数据失联即重置动作」的独立分支。若模型消息或其他输入失联,外层消息健康和下游自驾状态机可能触发退出,但那是另一层。

    若要把这类实验规划做成插件,至少应:用单调时钟或样本时间推进,而不是用消息循环数冒充秒数;建立显式的 idle → armed → active → abort/complete 转移,纵向失活、速度越界、数据失联与进程重启都要撤销动作;把实验规划插件与量产规划插件分开注册、隔离主题与权限,即使替换计划发布者,也不能成为 carControl 或 sendcan 的直接所有者。

    常见问题

    为什么是「第 61 次循环」而不是精确 3 秒?

    就绪门写的是 _ready_cnt > 3/DT_MDL。DT_MDL=0.05 秒,3/DT_MDL 等于 60,而比较是严格大于,所以至少需要 61 次合格循环才置为活动。动作推进也按主循环次数乘 DT_MDL 计时,轮询超时同样会让循环继续,因此它不是精确的 3.00 秒墙钟。

    活动后 longActive 变假,动作会立刻停止吗?

    函数本身不会在这一点重置 _active。活动后若 longActive 变假或速度越界,get_accel() 仍会进入 _step() 继续推进动作帧;这不等于执行器仍有输出,controlsd 有独立的 CC.longActive 门。

    longitudinalPlan.valid 为假时 controlsd 会丢弃目标值吗?

    不能这样断言。controlsd 的 publish() 处没有按 self.sm.valid['longitudinalPlan'] 包住目标读取;selfdrived 会对该主题做通信健康检查并可能产生 commIssue,但它与 controlsd 的字段消费是不同层。计划外层 valid=False 不必然让控制器直接丢弃数值。

    reset() 是完整的状态重置吗?

    不是。reset() 只清 _active、_action_frames、_action_index,不清 _ready_cnt、_repeated、_finished。若 repeat 之后就绪条件继续成立,下一个循环可能立即再次激活,因此不能把它当作完整状态重置。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/LONGITUDINAL_MANEUVER_STATE_MACHINE_DEEP_DIVE.md(取用了进程互斥、就绪门与第 61 次循环阈值、三路发布有效位与失效路径)。
    • 项目 02 —— openpilot-research/LONGITUDINAL_MANEUVER_TEST_MATRIX_DEEP_DIVE.md(关联的回归测试矩阵专题,用于区分发布控制流与测试覆盖,本文未展开其用例)。

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

    更新记录

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

  • 纵向 MPC 候选合成:10 秒 12 段时域与最小加速度选择

    结论:纵向规划器每帧把 MPC 目标加速度、巡航候选和实验模式的 e2e 候选放在一起,取最小加速度作为 aTarget,但 shouldStop 是对全部候选求或——所以两者可能来自不同候选,不能按单一来源重构。最终还要裁到车型接口常量 ACCEL_MIN=-3.5、ACCEL_MAX=2.0,再交给 LongControl。本文为固定源码快照 c8fb906 的静态分析,数学建模说明不等于数值收敛或车辆级认证。

    进程与消息链

    manager 进程表在非纵向机动实验的行车模式运行 plannerd。main() 等待 CarParams,以 modelV2 轮询;只有本轮模型主题更新才调用纵向规划并发布 longitudinalPlan,然后运行车道偏离检测、发布 driverAssistance。两个输出各自写 sm.all_checks() 作为顶层有效位,规划计算本身没有被该检查提前短路。longitudinalPlan 名义频率为 20 Hz,真实时延受输入消息年龄和求解耗时影响。

    输入包括 modelV2 模型动作与油门概率、radarState 两个前车、carState 车速/巡航速度/转角、controlsState 纵向状态与强制减速、vehicleParameters 转向零偏,以及 selfdriveState 模式与驾驶风格。MPC 本体把自车建模为位置、速度、加速度三维状态,以 jerk 为控制量。

    候选是怎么合成的

    LongitudinalPlanner.update() 同步车况、MPC、巡航和可选 e2e 候选,选择目标并推进期望车速滤波。它在 controlsState.forceDecel 时把巡航目标速度置零;纵向控制关闭、巡航速度未初始化或非自控纵向且尚未使能时重置内部目标。carControl.orientationNED 可用时以俯仰估计滑行加速度,否则以 ACCEL_MAX 作缺省值;vehicleParameters.angleOffsetDeg 从转角中扣除,用于非实验巡航候选的横向加速度预算。

    get_cruise_accel() 的巡航候选按车速限制正加速度,并在非实验模式下用 sqrt(max(a_total²-a_y²,0)) 限制弯中纵向余量;若 allow_throttle=False,还把正加速度上限向坡度估得的滑行加速度收紧。allow_throttle 来自模型 gasPressProbs[1] > 0.4 或车速 <= 2.5 m/s;若数组不足两项,代码回退概率 1.0。它的计算顺序是先将速度误差裁到巡航/弯道/滑行候选范围,再按上一帧 a_cruise 和 j_cruise * dt 限变化率。

    四项原函数 NumPy 探针确认了这种「先裁范围、后限变化率」的瞬态:10 m/s、上一帧 2.0 m/s² 时返回 1.94 m/s²,高于该速度的 1.2 m/s² 巡航上限;20 m/s 且弯道预算降为零时仍返回约 0.453 m/s²;禁油门滑行上限为 -0.3 m/s² 时可暂时返回 -0.07 m/s²。这是平滑历史带来的候选瞬态,不等于最终 aTarget 或车辆执行值必然越界。

    最终选择上,MPC 轨迹经 get_accel_from_plan() 按车型纵向延迟插值得到当前目标;再与巡航候选比较,实验模式增加 modelV2.action.desiredAcceleration 候选。代码取最小加速度,再裁到车型接口常量 ACCEL_MIN=-3.5、ACCEL_MAX=2.0;shouldStop 却对全部候选求或。因此 aTarget 与 shouldStop 可来自不同候选。controlsd controlsd 将两者交给 LongControl.update(),后者按 off/stopping/pid 状态与车型 PID 限值给执行器目标;不能把 aTarget 当作直接 CAN 帧。

    字段 来源 相位与边界
    aTarget MPC、巡航与实验模式 e2e 候选取最小加速度后再裁到车型常量。 不是直接 CAN 帧;需经 LongControl。
    shouldStop 对全部候选求或。 可与 aTarget 来自不同候选。
    allowThrottle 模型 gasPressProbs[1] > 0.4 或车速 <= 2.5 m/s。 只提示与巡航候选约束;controlsd 未把它当独立布尔门。
    消息 valid publish() 时写 sm.all_checks()。 与候选计算相分离,计算本身不被它短路。
    函数/组件 作用 关键边界
    get_cruise_accel() 按巡航速度差给目标加速度,非实验模式考虑弯道横向与滑行/油门约束,再做 jerk 限幅。 后置 jerk 限幅可使返回值暂时高于前面算出的候选上限。
    LongitudinalPlanner.update() 同步车况、MPC、巡航与可选 e2e 候选,选择目标并推进期望车速滤波。 允许在输入检查失败时计算;valid 只在发布时反映 sm.all_checks()。
    LongitudinalPlanner.publish() 发布速度/加速度/jerk 轨迹、aTarget、shouldStop、FCW、来源与油门提示。 hasLead 取 leadOne.present,不表达雷达消息有效性。
    get_T_FOLLOW() 选择跟车时距:宽松 1.75 s、标准 1.45 s、激进 1.25 s。 个性化只改变优化参数,不改变独立安全网关策略。

    MPC 时域与跟车时距

    gen_long_model() 在构建期由 CasADi/acados 生成求解器,运行时不是 Python 手工积分器。gen_long_ocp() 建立 12 段、10 秒非均匀预测时域,设置距离/jerk 成本与速度、加速度、危险距离软约束;约束与权重在运行时更新,不能只看静态模型推断车辆一定在物理极限内。get_T_FOLLOW() 按驾驶风格选择 1.75 s、1.45 s 或 1.25 s 的跟车时距。

    process_lead() 处理前车状态、裁剪异常值并外推轨迹;无前车时注入虚拟高速前车。六项原 MPC 前车方法隔离探针确认:无目标时首点为前方 50 m、车速比自车高 10 m/s 的虚拟前车;近目标距离会被可刹距离公式抬高,输入加速度被裁到 [-10,5] m/s²。LongitudinalMpc.update() 把两个前车换算成等效静止障碍,逐节点选择更近约束,运行求解并累计 FCW 近碰撞帧;source 取第零节点最近障碍,后续节点可换前车,FCW 只用 leadOne 且要求 modelProb>0.9。探针确认即使后续节点的逐时刻最小障碍已切换到另一前车,source 只由第零节点决定。set_cur_state() 与上次速度差严格大于 2 m/s 时重设所有 solver 节点状态,等于 2 m/s 不重设。

    求解失败与 FCW 边界

    LongitudinalMpc.run() 在求解状态非零时先读取本次解,再按单调时钟判断是否记录警告并调用 reset();LongitudinalPlanner.update() 随后仍插值当前(已清零的)速度/加速度/jerk 轨迹并参与候选选择,没有专门的「求解失败」消息位。reset() 同时将 solve_time、solution_status、crash_cnt 与 last_cloudlog_t 置零;因此告警时间戳不跨失败保存,当单调时钟已超过 5 秒时,连续失败可每次记录,而非真正跨失败限频。发布的求解耗时为清零后的值,原始失败状态没有发布。

    FCW 侧,LongitudinalMpc.update() 在 5 秒预测窗口内存在距离小于 0.25 m 且首个前车模型概率 >0.9 时累加 crash_cnt;规划器要求计数 >2 且车辆未静止才发布 FCW。selfdrived 还会结合另一条模型硬制动预测生成 FCW 事件,两者不能混为单一碰撞检测器。探针还确认:第一前车模型概率从 0.91 降到严格边界 0.9 时计数清零;dRel=NaN 可继续形成 NaN 位置轨迹,当前纯输入处理层没有有限性校验。

    常见问题

    为什么 aTarget 和 shouldStop 可能来自不同的候选?

    因为最终 aTarget 是「取最小加速度」,而 shouldStop 是对全部候选「求或」。探针确认:MPC 的 -0.5 m/s² 目标获选时,较大的 e2e 候选仍可提供 shouldStop=True。

    跟车距离有哪几档?

    get_T_FOLLOW() 提供宽松 1.75 s、标准 1.45 s、激进 1.25 s 三档;个性化只改变 MPC 的优化参数,不改变独立的安全网关策略。

    MPC 求解失败会怎样?

    run() 遇非零求解状态会调用 reset(),清零轨迹、计数和状态;规划器随后仍插值已清零的轨迹参与候选选择,且没有专门的「求解失败」消息位,发布的求解耗时也是清零后的值。

    allowThrottle=False 是不是就等于禁止加速?

    不完全是。allowThrottle 被发布供 UI 调整显示;本快照 controlsd.state_control() 只显式消费 aTarget/shouldStop,未把 allowThrottle 作为独立布尔门传给 LongControl。它对正常巡航候选的限幅有作用,但不能据此断言所有候选都被禁止正加速度。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/LONGITUDINAL_PLANNER_MPC_DEEP_DIVE.md(plannerd 消息链、候选合成、MPC 时域与求解失败边界)。
    • 项目 02 —— 同目录探针记录:四项巡航数值、四项规划器候选、六项 MPC 前车、四项求解器生命周期隔离探针结论。

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

    更新记录

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